comparison trafficintelligence/metadata.py @ 1028:cc5cb04b04b0

major update using the trafficintelligence package name and install through pip
author Nicolas Saunier <nicolas.saunier@polymtl.ca>
date Fri, 15 Jun 2018 11:19:10 -0400
parents python/metadata.py@75601be6019f
children 9d4a06f49cb8
comparison
equal deleted inserted replaced
1027:6129296848d3 1028:cc5cb04b04b0
1 from datetime import datetime, timedelta
2 from os import path, listdir, sep
3 from math import floor
4
5 from numpy import zeros, loadtxt, array
6
7 from sqlalchemy import orm, create_engine, Column, Integer, Float, DateTime, String, ForeignKey, Boolean, Interval
8 from sqlalchemy.orm import relationship, backref, sessionmaker
9 from sqlalchemy.ext.declarative import declarative_base
10
11 from trafficintelligence.utils import datetimeFormat, removeExtension, getExtension, TimeConverter
12 from trafficintelligence.cvutils import computeUndistortMaps, videoFilenameExtensions, infoVideo
13 from trafficintelligence.moving import TimeInterval
14
15 """
16 Metadata to describe how video data and configuration files for video analysis are stored
17
18 Typical example is
19
20 site1/view1/2012-06-01/video.avi
21 /2012-06-02/video.avi
22 ...
23 /view2/2012-06-01/video.avi
24 /2012-06-02/video.avi
25 ...
26
27 - where site1 is the path to the directory containing all information pertaining to the site,
28 relative to directory of the SQLite file storing the metadata
29 represented by Site class
30 (can contain for example the aerial or map image of the site, used for projection)
31
32 - view1 is the directory for the first camera field of view (camera fixed position) at site site1
33 represented by CameraView class
34 (can contain for example the homography file, mask file and tracking configuration file)
35
36 - YYYY-MM-DD is the directory containing all the video files for that day
37 with camera view view1 at site site1
38
39
40 """
41
42 Base = declarative_base()
43
44 class Site(Base):
45 __tablename__ = 'sites'
46 idx = Column(Integer, primary_key=True)
47 name = Column(String) # path to directory containing all site information (in subdirectories), relative to the database position
48 description = Column(String) # longer names, eg intersection of road1 and road2
49 xcoordinate = Column(Float) # ideally moving.Point, but needs to be
50 ycoordinate = Column(Float)
51 mapImageFilename = Column(String) # path to map image file, relative to site name, ie sitename/mapImageFilename
52 nUnitsPerPixel = Column(Float) # number of units of distance per pixel in map image
53 worldDistanceUnit = Column(String, default = 'm') # make sure it is default in the database
54
55 def __init__(self, name, description = "", xcoordinate = None, ycoordinate = None, mapImageFilename = None, nUnitsPerPixel = 1., worldDistanceUnit = 'm'):
56 self.name = name
57 self.description = description
58 self.xcoordinate = xcoordinate
59 self.ycoordinate = ycoordinate
60 self.mapImageFilename = mapImageFilename
61 self.nUnitsPerPixel = nUnitsPerPixel
62 self.worldDistanceUnit = worldDistanceUnit
63
64 def getPath(self):
65 return self.name
66
67 def getMapImageFilename(self, relativeToSiteFilename = True):
68 if relativeToSiteFilename:
69 return path.join(self.getPath(), self.mapImageFilename)
70 else:
71 return self.mapImageFilename
72
73
74 class EnvironementalFactors(Base):
75 '''Represents any environmental factors that may affect the results, in particular
76 * changing weather conditions
77 * changing road configuration, geometry, signalization, etc.
78 ex: sunny, rainy, before counter-measure, after counter-measure'''
79 __tablename__ = 'environmental_factors'
80 idx = Column(Integer, primary_key=True)
81 startTime = Column(DateTime)
82 endTime = Column(DateTime)
83 description = Column(String) # eg sunny, before, after
84 siteIdx = Column(Integer, ForeignKey('sites.idx'))
85
86 site = relationship("Site", backref = backref('environmentalFactors'))
87
88 def __init__(self, startTime, endTime, description, site):
89 'startTime is passed as string in utils.datetimeFormat, eg 2011-06-22 10:00:39'
90 self.startTime = datetime.strptime(startTime, datetimeFormat)
91 self.endTime = datetime.strptime(endTime, datetimeFormat)
92 self.description = description
93 self.site = site
94
95 class CameraType(Base):
96 ''' Represents parameters of the specific camera used.
97
98 Taken and adapted from tvalib'''
99 __tablename__ = 'camera_types'
100 idx = Column(Integer, primary_key=True)
101 name = Column(String)
102 resX = Column(Integer)
103 resY = Column(Integer)
104 frameRate = Column(Float)
105 frameRateTimeUnit = Column(String, default = 's')
106 intrinsicCameraMatrixStr = Column(String)
107 distortionCoefficientsStr = Column(String)
108
109 def __init__(self, name, resX, resY, frameRate, frameRateTimeUnit = 's', trackingConfigurationFilename = None, intrinsicCameraFilename = None, intrinsicCameraMatrix = None, distortionCoefficients = None):
110 self.name = name
111 self.resX = resX
112 self.resY = resY
113 self.frameRate = frameRate
114 self.frameRateTimeUnit = frameRateTimeUnit
115 self.intrinsicCameraMatrix = None # should be np.array
116 self.distortionCoefficients = None # list
117
118 if trackingConfigurationFilename is not None:
119 from storage import ProcessParameters
120 params = ProcessParameters(trackingConfigurationFilename)
121 self.intrinsicCameraMatrix = params.intrinsicCameraMatrix
122 self.distortionCoefficients = params.distortionCoefficients
123 elif intrinsicCameraFilename is not None:
124 self.intrinsicCameraMatrix = loadtxt(intrinsicCameraFilename)
125 self.distortionCoefficients = distortionCoefficients
126 else:
127 self.intrinsicCameraMatrix = intrinsicCameraMatrix
128 self.distortionCoefficients = distortionCoefficients
129
130 if self.intrinsicCameraMatrix is not None:
131 self.intrinsicCameraMatrixStr = str(self.intrinsicCameraMatrix.tolist())
132 if self.distortionCoefficients is not None and len(self.distortionCoefficients) == 5:
133 self.distortionCoefficientsStr = str(self.distortionCoefficients)
134
135 @orm.reconstructor
136 def initOnLoad(self):
137 if self.intrinsicCameraMatrixStr is not None:
138 from ast import literal_eval
139 self.intrinsicCameraMatrix = array(literal_eval(self.intrinsicCameraMatrixStr))
140 else:
141 self.intrinsicCameraMatrix = None
142 if self.distortionCoefficientsStr is not None:
143 self.distortionCoefficients = literal_eval(self.distortionCoefficientsStr)
144 else:
145 self.distortionCoefficients = None
146
147 def computeUndistortMaps(self, undistortedImageMultiplication = None):
148 if undistortedImageMultiplication is not None and self.intrinsicCameraMatrix is not None and self.distortionCoefficients is not None:
149 [self.map1, self.map2], newCameraMatrix = computeUndistortMaps(self.resX, self.resY, undistortedImageMultiplication, self.intrinsicCameraMatrix, self.distortionCoefficients)
150 else:
151 self.map1 = None
152 self.map2 = None
153
154 @staticmethod
155 def getCameraType(session, cameraTypeId, resX = None):
156 'Returns the site(s) matching the index or the name'
157 if str.isdigit(cameraTypeId):
158 return session.query(CameraType).filter(CameraType.idx == int(cameraTypeId)).all()
159 else:
160 if resX is not None:
161 return session.query(CameraType).filter(CameraType.name.like('%'+cameraTypeId+'%')).filter(CameraType.resX == resX).all()
162 else:
163 return session.query(CameraType).filter(CameraType.name.like('%'+cameraTypeId+'%')).all()
164
165 # class SiteDescription(Base): # list of lines and polygons describing the site, eg for sidewalks, center lines
166
167 class CameraView(Base):
168 __tablename__ = 'camera_views'
169 idx = Column(Integer, primary_key=True)
170 description = Column(String)
171 homographyFilename = Column(String) # path to homograph file, relative to the site name
172 siteIdx = Column(Integer, ForeignKey('sites.idx'))
173 cameraTypeIdx = Column(Integer, ForeignKey('camera_types.idx'))
174 trackingConfigurationFilename = Column(String) # path to configuration .cfg file, relative to site name
175 maskFilename = Column(String) # path to mask file, relative to site name
176 virtual = Column(Boolean) # indicates it is not a real camera view, eg merged
177
178 site = relationship("Site", backref = backref('cameraViews'))
179 cameraType = relationship('CameraType', backref = backref('cameraViews'))
180
181 def __init__(self, description, homographyFilename, site, cameraType, trackingConfigurationFilename, maskFilename, virtual = False):
182 self.description = description
183 self.homographyFilename = homographyFilename
184 self.site = site
185 self.cameraType = cameraType
186 self.trackingConfigurationFilename = trackingConfigurationFilename
187 self.maskFilename = maskFilename
188 self.virtual = virtual
189
190 def getHomographyFilename(self, relativeToSiteFilename = True):
191 if relativeToSiteFilename:
192 return path.join(self.site.getPath(), self.homographyFilename)
193 else:
194 return self.homographyFilename
195
196 def getTrackingConfigurationFilename(self, relativeToSiteFilename = True):
197 if relativeToSiteFilename:
198 return path.join(self.site.getPath(), self.trackingConfigurationFilename)
199 else:
200 return self.trackingConfigurationFilename
201
202 def getMaskFilename(self, relativeToSiteFilename = True):
203 if relativeToSiteFilename:
204 return path.join(self.site.getPath(), self.maskFilename)
205 else:
206 return self.maskFilename
207
208 def getTrackingParameters(self):
209 return ProcessParameters(self.getTrackingConfigurationFilename())
210
211 def getHomographyDistanceUnit(self):
212 return self.site.worldDistanceUnit
213
214 # class Alignment(Base):
215 # __tablename__ = 'alignments'
216 # idx = Column(Integer, primary_key=True)
217 # siteIdx = Column(Integer, ForeignKey('sites.idx'))
218
219 # cameraView = relationship("Site", backref = backref('alignments'))
220
221 # def __init__(self, cameraView):
222 # self.cameraView = cameraView
223
224 # class Point(Base):
225 # __tablename__ = 'points'
226 # alignmentIdx = Column(Integer, ForeignKey('alignments.idx'), primary_key=True)
227 # index = Column(Integer, primary_key=True) # order of points in this alignment
228 # x = Column(Float)
229 # y = Column(Float)
230
231 # alignment = relationship("Alignment", backref = backref('points', order_by = index))
232
233 # def __init__(self, alignmentIdx, index, x, y):
234 # self.alignmentIdx = alignmentIdx
235 # self.index = index
236 # self.x = x
237 # self.y = y
238
239 class VideoSequence(Base):
240 __tablename__ = 'video_sequences'
241 idx = Column(Integer, primary_key=True)
242 name = Column(String) # path to the video file relative to the the site name
243 startTime = Column(DateTime)
244 duration = Column(Interval) # video sequence duration
245 databaseFilename = Column(String) # path to the database file relative to the the site name
246 virtual = Column(Boolean) # indicates it is not a real video sequence (no video file), eg merged
247 cameraViewIdx = Column(Integer, ForeignKey('camera_views.idx'))
248
249 cameraView = relationship("CameraView", backref = backref('videoSequences', order_by = idx))
250
251 def __init__(self, name, startTime, duration, cameraView, databaseFilename = None, virtual = False):
252 '''startTime is passed as string in utils.datetimeFormat, eg 2011-06-22 10:00:39
253 duration is a timedelta object'''
254 self.name = name
255 if isinstance(startTime, str):
256 self.startTime = datetime.strptime(startTime, datetimeFormat)
257 else:
258 self.startTime = startTime
259 self.duration = duration
260 self.cameraView = cameraView
261 if databaseFilename is None and len(self.name) > 0:
262 self.databaseFilename = removeExtension(self.name)+'.sqlite'
263 else:
264 self.databaseFilename = databaseFilename
265 self.virtual = virtual
266
267 def getVideoSequenceFilename(self, relativeToSiteFilename = True):
268 if relativeToSiteFilename:
269 return path.join(self.cameraView.site.getPath(), self.name)
270 else:
271 return self.name
272
273 def getDatabaseFilename(self, relativeToSiteFilename = True):
274 if relativeToSiteFilename:
275 return path.join(self.cameraView.site.getPath(), self.databaseFilename)
276 else:
277 return self.databaseFilename
278
279 def getTimeInterval(self):
280 return TimeInterval(self.startTime, self.startTime+self.duration)
281
282 def containsInstant(self, instant):
283 'instant is a datetime'
284 return self.startTime <= instant and self.startTime+self.duration
285
286 def intersection(self, startTime, endTime):
287 'returns the moving.TimeInterval intersection with [startTime, endTime]'
288 return TimeInterval.intersection(self.getTimeInterval(), TimeInterval(startTime, endTime))
289
290 def getFrameNum(self, instant):
291 'Warning, there is no check of correct time units'
292 if self.containsInstant(instant):
293 return int(floor((instant-self.startTime).seconds*self.cameraView.cameraType.frameRate))
294 else:
295 return None
296
297 class TrackingAnnotation(Base):
298 __tablename__ = 'tracking_annotations'
299 idx = Column(Integer, primary_key=True)
300 description = Column(String) # description
301 groundTruthFilename = Column(String)
302 firstFrameNum = Column(Integer) # first frame num of annotated data (could be computed on less data)
303 lastFrameNum = Column(Integer)
304 videoSequenceIdx = Column(Integer, ForeignKey('video_sequences.idx'))
305 maskFilename = Column(String) # path to mask file (can be different from camera view, for annotations), relative to site name
306 undistorted = Column(Boolean) # indicates whether the annotations were done in undistorted video space
307
308 videoSequence = relationship("VideoSequence", backref = backref('trackingAnnotations'))
309
310 def __init__(self, description, groundTruthFilename, firstFrameNum, lastFrameNum, videoSequence, maskFilename, undistorted = True):
311 self.description = description
312 self.groundTruthFilename = groundTruthFilename
313 self.firstFrameNum = firstFrameNum
314 self.lastFrameNum = lastFrameNum
315 self.videoSequence = videoSequence
316 self.undistorted = undistorted
317 self.maskFilename = maskFilename
318
319 def getGroundTruthFilename(self, relativeToSiteFilename = True):
320 if relativeToSiteFilename:
321 return path.join(self.videoSequence.cameraView.site.getPath(), self.groundTruthFilename)
322 else:
323 return self.groundTruthFilename
324
325 def getMaskFilename(self, relativeToSiteFilename = True):
326 if relativeToSiteFilename:
327 return path.join(self.videoSequence.cameraView.site.getPath(), self.maskFilename)
328 else:
329 return self.maskFilename
330
331 def getTimeInterval(self):
332 return TimeInterval(self.firstFrameNum, self.lastFrameNum)
333
334 # add class for Analysis: foreign key VideoSequenceId, dataFilename, configFilename (get the one from camera view by default), mask? (no, can be referenced in the tracking cfg file)
335
336 # class Analysis(Base): # parameters necessary for processing the data: free form
337 # eg bounding box depends on camera view, tracking configuration depends on camera view
338 # results: sqlite
339
340 def createDatabase(filename):
341 'creates a session to query the filename'
342 engine = create_engine('sqlite:///'+filename)
343 Base.metadata.create_all(engine)
344 Session = sessionmaker(bind=engine)
345 return Session()
346
347 def connectDatabase(filename):
348 'creates a session to query the filename'
349 engine = create_engine('sqlite:///'+filename)
350 Session = sessionmaker(bind=engine)
351 return Session()
352
353 def getSite(session, siteId = None, name = None, description = None):
354 'Returns the site(s) matching the index or the name'
355 if siteId is not None:
356 return session.query(Site).filter(Site.idx == int(siteId)).all()
357 elif name is not None:
358 return session.query(Site).filter(Site.description.like('%'+name+'%')).all()
359 elif description is not None:
360 return session.query(Site).filter(Site.description.like('%'+description+'%')).all()
361 else:
362 print('No siteId, name or description have been provided to the function')
363 return []
364
365 def getCameraView(session, viewId):
366 'Returns the site(s) matching the index'
367 return session.query(CameraView).filter(CameraView.idx == int(viewId)).first()
368
369 def initializeSites(session, directoryName, nViewsPerSite = 1):
370 '''Initializes default site objects and n camera views per site
371
372 eg somedirectory/montreal/ contains intersection1, intersection2, etc.
373 The site names would be somedirectory/montreal/intersection1, somedirectory/montreal/intersection2, etc.
374 The views should be directories in somedirectory/montreal/intersection1'''
375 sites = []
376 cameraViews = []
377 names = sorted(listdir(directoryName))
378 for name in names:
379 if path.isdir(directoryName+sep+name):
380 sites.append(Site(directoryName+sep+name, None))
381 for cameraViewIdx in range(1, nViewsPerSite+1):
382 cameraViews.append(CameraView('view{}'.format(cameraViewIdx), None, sites[-1], None, None, None))
383 session.add_all(sites)
384 session.add_all(cameraViews)
385 session.commit()
386
387 def initializeVideos(session, cameraView, directoryName, startTime = None, datetimeFormat = None):
388 '''Initializes videos with time or tries to guess it from filename
389 directoryName should contain the videos to find and be the relative path from the site location'''
390 names = sorted(listdir(directoryName))
391 videoSequences = []
392 if datetimeFormat is not None:
393 timeConverter = TimeConverter(datetimeFormat)
394 for name in names:
395 prefix = removeExtension(name)
396 extension = getExtension(name)
397 if extension in videoFilenameExtensions:
398 if datetimeFormat is not None:
399 from argparse import ArgumentTypeError
400 try:
401 t1 = timeConverter.convert(name[:name.rfind('_')])
402 print('DB time {} / Time from filename {}'.format(startTime, t1))
403 except ArgumentTypeError as e:
404 print('File format error for time {} (prefix {})'.format(name, prefix))
405 vidinfo = infoVideo(directoryName+sep+name)
406 duration = vidinfo['number of frames']#timedelta(minutes = 27, seconds = 33)
407 fps = vidinfo['fps']
408 duration = timedelta(seconds=duration/fps)
409 videoSequences.append(VideoSequence(directoryName+sep+name, startTime, duration, cameraView, directoryName+sep+prefix+'.sqlite'))
410 startTime += duration
411 session.add_all(videoSequences)
412 session.commit()
413
414 # management
415 # TODO need to be able to copy everything from a site from one sqlite to another, and delete everything attached to a site