Mercurial > hg > nsaunier > 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 |
