Mercurial Hosting > traffic-intelligence
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 |