Mercurial Hosting > traffic-intelligence
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/trafficintelligence/metadata.py Fri Jun 15 11:19:10 2018 -0400 @@ -0,0 +1,415 @@ +from datetime import datetime, timedelta +from os import path, listdir, sep +from math import floor + +from numpy import zeros, loadtxt, array + +from sqlalchemy import orm, create_engine, Column, Integer, Float, DateTime, String, ForeignKey, Boolean, Interval +from sqlalchemy.orm import relationship, backref, sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +from trafficintelligence.utils import datetimeFormat, removeExtension, getExtension, TimeConverter +from trafficintelligence.cvutils import computeUndistortMaps, videoFilenameExtensions, infoVideo +from trafficintelligence.moving import TimeInterval + +""" +Metadata to describe how video data and configuration files for video analysis are stored + +Typical example is + +site1/view1/2012-06-01/video.avi + /2012-06-02/video.avi + ... + /view2/2012-06-01/video.avi + /2012-06-02/video.avi + ... + +- where site1 is the path to the directory containing all information pertaining to the site, +relative to directory of the SQLite file storing the metadata +represented by Site class +(can contain for example the aerial or map image of the site, used for projection) + +- view1 is the directory for the first camera field of view (camera fixed position) at site site1 +represented by CameraView class +(can contain for example the homography file, mask file and tracking configuration file) + +- YYYY-MM-DD is the directory containing all the video files for that day +with camera view view1 at site site1 + + +""" + +Base = declarative_base() + +class Site(Base): + __tablename__ = 'sites' + idx = Column(Integer, primary_key=True) + name = Column(String) # path to directory containing all site information (in subdirectories), relative to the database position + description = Column(String) # longer names, eg intersection of road1 and road2 + xcoordinate = Column(Float) # ideally moving.Point, but needs to be + ycoordinate = Column(Float) + mapImageFilename = Column(String) # path to map image file, relative to site name, ie sitename/mapImageFilename + nUnitsPerPixel = Column(Float) # number of units of distance per pixel in map image + worldDistanceUnit = Column(String, default = 'm') # make sure it is default in the database + + def __init__(self, name, description = "", xcoordinate = None, ycoordinate = None, mapImageFilename = None, nUnitsPerPixel = 1., worldDistanceUnit = 'm'): + self.name = name + self.description = description + self.xcoordinate = xcoordinate + self.ycoordinate = ycoordinate + self.mapImageFilename = mapImageFilename + self.nUnitsPerPixel = nUnitsPerPixel + self.worldDistanceUnit = worldDistanceUnit + + def getPath(self): + return self.name + + def getMapImageFilename(self, relativeToSiteFilename = True): + if relativeToSiteFilename: + return path.join(self.getPath(), self.mapImageFilename) + else: + return self.mapImageFilename + + +class EnvironementalFactors(Base): + '''Represents any environmental factors that may affect the results, in particular + * changing weather conditions + * changing road configuration, geometry, signalization, etc. + ex: sunny, rainy, before counter-measure, after counter-measure''' + __tablename__ = 'environmental_factors' + idx = Column(Integer, primary_key=True) + startTime = Column(DateTime) + endTime = Column(DateTime) + description = Column(String) # eg sunny, before, after + siteIdx = Column(Integer, ForeignKey('sites.idx')) + + site = relationship("Site", backref = backref('environmentalFactors')) + + def __init__(self, startTime, endTime, description, site): + 'startTime is passed as string in utils.datetimeFormat, eg 2011-06-22 10:00:39' + self.startTime = datetime.strptime(startTime, datetimeFormat) + self.endTime = datetime.strptime(endTime, datetimeFormat) + self.description = description + self.site = site + +class CameraType(Base): + ''' Represents parameters of the specific camera used. + + Taken and adapted from tvalib''' + __tablename__ = 'camera_types' + idx = Column(Integer, primary_key=True) + name = Column(String) + resX = Column(Integer) + resY = Column(Integer) + frameRate = Column(Float) + frameRateTimeUnit = Column(String, default = 's') + intrinsicCameraMatrixStr = Column(String) + distortionCoefficientsStr = Column(String) + + def __init__(self, name, resX, resY, frameRate, frameRateTimeUnit = 's', trackingConfigurationFilename = None, intrinsicCameraFilename = None, intrinsicCameraMatrix = None, distortionCoefficients = None): + self.name = name + self.resX = resX + self.resY = resY + self.frameRate = frameRate + self.frameRateTimeUnit = frameRateTimeUnit + self.intrinsicCameraMatrix = None # should be np.array + self.distortionCoefficients = None # list + + if trackingConfigurationFilename is not None: + from storage import ProcessParameters + params = ProcessParameters(trackingConfigurationFilename) + self.intrinsicCameraMatrix = params.intrinsicCameraMatrix + self.distortionCoefficients = params.distortionCoefficients + elif intrinsicCameraFilename is not None: + self.intrinsicCameraMatrix = loadtxt(intrinsicCameraFilename) + self.distortionCoefficients = distortionCoefficients + else: + self.intrinsicCameraMatrix = intrinsicCameraMatrix + self.distortionCoefficients = distortionCoefficients + + if self.intrinsicCameraMatrix is not None: + self.intrinsicCameraMatrixStr = str(self.intrinsicCameraMatrix.tolist()) + if self.distortionCoefficients is not None and len(self.distortionCoefficients) == 5: + self.distortionCoefficientsStr = str(self.distortionCoefficients) + + @orm.reconstructor + def initOnLoad(self): + if self.intrinsicCameraMatrixStr is not None: + from ast import literal_eval + self.intrinsicCameraMatrix = array(literal_eval(self.intrinsicCameraMatrixStr)) + else: + self.intrinsicCameraMatrix = None + if self.distortionCoefficientsStr is not None: + self.distortionCoefficients = literal_eval(self.distortionCoefficientsStr) + else: + self.distortionCoefficients = None + + def computeUndistortMaps(self, undistortedImageMultiplication = None): + if undistortedImageMultiplication is not None and self.intrinsicCameraMatrix is not None and self.distortionCoefficients is not None: + [self.map1, self.map2], newCameraMatrix = computeUndistortMaps(self.resX, self.resY, undistortedImageMultiplication, self.intrinsicCameraMatrix, self.distortionCoefficients) + else: + self.map1 = None + self.map2 = None + + @staticmethod + def getCameraType(session, cameraTypeId, resX = None): + 'Returns the site(s) matching the index or the name' + if str.isdigit(cameraTypeId): + return session.query(CameraType).filter(CameraType.idx == int(cameraTypeId)).all() + else: + if resX is not None: + return session.query(CameraType).filter(CameraType.name.like('%'+cameraTypeId+'%')).filter(CameraType.resX == resX).all() + else: + return session.query(CameraType).filter(CameraType.name.like('%'+cameraTypeId+'%')).all() + +# class SiteDescription(Base): # list of lines and polygons describing the site, eg for sidewalks, center lines + +class CameraView(Base): + __tablename__ = 'camera_views' + idx = Column(Integer, primary_key=True) + description = Column(String) + homographyFilename = Column(String) # path to homograph file, relative to the site name + siteIdx = Column(Integer, ForeignKey('sites.idx')) + cameraTypeIdx = Column(Integer, ForeignKey('camera_types.idx')) + trackingConfigurationFilename = Column(String) # path to configuration .cfg file, relative to site name + maskFilename = Column(String) # path to mask file, relative to site name + virtual = Column(Boolean) # indicates it is not a real camera view, eg merged + + site = relationship("Site", backref = backref('cameraViews')) + cameraType = relationship('CameraType', backref = backref('cameraViews')) + + def __init__(self, description, homographyFilename, site, cameraType, trackingConfigurationFilename, maskFilename, virtual = False): + self.description = description + self.homographyFilename = homographyFilename + self.site = site + self.cameraType = cameraType + self.trackingConfigurationFilename = trackingConfigurationFilename + self.maskFilename = maskFilename + self.virtual = virtual + + def getHomographyFilename(self, relativeToSiteFilename = True): + if relativeToSiteFilename: + return path.join(self.site.getPath(), self.homographyFilename) + else: + return self.homographyFilename + + def getTrackingConfigurationFilename(self, relativeToSiteFilename = True): + if relativeToSiteFilename: + return path.join(self.site.getPath(), self.trackingConfigurationFilename) + else: + return self.trackingConfigurationFilename + + def getMaskFilename(self, relativeToSiteFilename = True): + if relativeToSiteFilename: + return path.join(self.site.getPath(), self.maskFilename) + else: + return self.maskFilename + + def getTrackingParameters(self): + return ProcessParameters(self.getTrackingConfigurationFilename()) + + def getHomographyDistanceUnit(self): + return self.site.worldDistanceUnit + +# class Alignment(Base): +# __tablename__ = 'alignments' +# idx = Column(Integer, primary_key=True) +# siteIdx = Column(Integer, ForeignKey('sites.idx')) + +# cameraView = relationship("Site", backref = backref('alignments')) + +# def __init__(self, cameraView): +# self.cameraView = cameraView + +# class Point(Base): +# __tablename__ = 'points' +# alignmentIdx = Column(Integer, ForeignKey('alignments.idx'), primary_key=True) +# index = Column(Integer, primary_key=True) # order of points in this alignment +# x = Column(Float) +# y = Column(Float) + +# alignment = relationship("Alignment", backref = backref('points', order_by = index)) + +# def __init__(self, alignmentIdx, index, x, y): +# self.alignmentIdx = alignmentIdx +# self.index = index +# self.x = x +# self.y = y + +class VideoSequence(Base): + __tablename__ = 'video_sequences' + idx = Column(Integer, primary_key=True) + name = Column(String) # path to the video file relative to the the site name + startTime = Column(DateTime) + duration = Column(Interval) # video sequence duration + databaseFilename = Column(String) # path to the database file relative to the the site name + virtual = Column(Boolean) # indicates it is not a real video sequence (no video file), eg merged + cameraViewIdx = Column(Integer, ForeignKey('camera_views.idx')) + + cameraView = relationship("CameraView", backref = backref('videoSequences', order_by = idx)) + + def __init__(self, name, startTime, duration, cameraView, databaseFilename = None, virtual = False): + '''startTime is passed as string in utils.datetimeFormat, eg 2011-06-22 10:00:39 + duration is a timedelta object''' + self.name = name + if isinstance(startTime, str): + self.startTime = datetime.strptime(startTime, datetimeFormat) + else: + self.startTime = startTime + self.duration = duration + self.cameraView = cameraView + if databaseFilename is None and len(self.name) > 0: + self.databaseFilename = removeExtension(self.name)+'.sqlite' + else: + self.databaseFilename = databaseFilename + self.virtual = virtual + + def getVideoSequenceFilename(self, relativeToSiteFilename = True): + if relativeToSiteFilename: + return path.join(self.cameraView.site.getPath(), self.name) + else: + return self.name + + def getDatabaseFilename(self, relativeToSiteFilename = True): + if relativeToSiteFilename: + return path.join(self.cameraView.site.getPath(), self.databaseFilename) + else: + return self.databaseFilename + + def getTimeInterval(self): + return TimeInterval(self.startTime, self.startTime+self.duration) + + def containsInstant(self, instant): + 'instant is a datetime' + return self.startTime <= instant and self.startTime+self.duration + + def intersection(self, startTime, endTime): + 'returns the moving.TimeInterval intersection with [startTime, endTime]' + return TimeInterval.intersection(self.getTimeInterval(), TimeInterval(startTime, endTime)) + + def getFrameNum(self, instant): + 'Warning, there is no check of correct time units' + if self.containsInstant(instant): + return int(floor((instant-self.startTime).seconds*self.cameraView.cameraType.frameRate)) + else: + return None + +class TrackingAnnotation(Base): + __tablename__ = 'tracking_annotations' + idx = Column(Integer, primary_key=True) + description = Column(String) # description + groundTruthFilename = Column(String) + firstFrameNum = Column(Integer) # first frame num of annotated data (could be computed on less data) + lastFrameNum = Column(Integer) + videoSequenceIdx = Column(Integer, ForeignKey('video_sequences.idx')) + maskFilename = Column(String) # path to mask file (can be different from camera view, for annotations), relative to site name + undistorted = Column(Boolean) # indicates whether the annotations were done in undistorted video space + + videoSequence = relationship("VideoSequence", backref = backref('trackingAnnotations')) + + def __init__(self, description, groundTruthFilename, firstFrameNum, lastFrameNum, videoSequence, maskFilename, undistorted = True): + self.description = description + self.groundTruthFilename = groundTruthFilename + self.firstFrameNum = firstFrameNum + self.lastFrameNum = lastFrameNum + self.videoSequence = videoSequence + self.undistorted = undistorted + self.maskFilename = maskFilename + + def getGroundTruthFilename(self, relativeToSiteFilename = True): + if relativeToSiteFilename: + return path.join(self.videoSequence.cameraView.site.getPath(), self.groundTruthFilename) + else: + return self.groundTruthFilename + + def getMaskFilename(self, relativeToSiteFilename = True): + if relativeToSiteFilename: + return path.join(self.videoSequence.cameraView.site.getPath(), self.maskFilename) + else: + return self.maskFilename + + def getTimeInterval(self): + return TimeInterval(self.firstFrameNum, self.lastFrameNum) + +# 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) + +# class Analysis(Base): # parameters necessary for processing the data: free form +# eg bounding box depends on camera view, tracking configuration depends on camera view +# results: sqlite + +def createDatabase(filename): + 'creates a session to query the filename' + engine = create_engine('sqlite:///'+filename) + Base.metadata.create_all(engine) + Session = sessionmaker(bind=engine) + return Session() + +def connectDatabase(filename): + 'creates a session to query the filename' + engine = create_engine('sqlite:///'+filename) + Session = sessionmaker(bind=engine) + return Session() + +def getSite(session, siteId = None, name = None, description = None): + 'Returns the site(s) matching the index or the name' + if siteId is not None: + return session.query(Site).filter(Site.idx == int(siteId)).all() + elif name is not None: + return session.query(Site).filter(Site.description.like('%'+name+'%')).all() + elif description is not None: + return session.query(Site).filter(Site.description.like('%'+description+'%')).all() + else: + print('No siteId, name or description have been provided to the function') + return [] + +def getCameraView(session, viewId): + 'Returns the site(s) matching the index' + return session.query(CameraView).filter(CameraView.idx == int(viewId)).first() + +def initializeSites(session, directoryName, nViewsPerSite = 1): + '''Initializes default site objects and n camera views per site + + eg somedirectory/montreal/ contains intersection1, intersection2, etc. + The site names would be somedirectory/montreal/intersection1, somedirectory/montreal/intersection2, etc. + The views should be directories in somedirectory/montreal/intersection1''' + sites = [] + cameraViews = [] + names = sorted(listdir(directoryName)) + for name in names: + if path.isdir(directoryName+sep+name): + sites.append(Site(directoryName+sep+name, None)) + for cameraViewIdx in range(1, nViewsPerSite+1): + cameraViews.append(CameraView('view{}'.format(cameraViewIdx), None, sites[-1], None, None, None)) + session.add_all(sites) + session.add_all(cameraViews) + session.commit() + +def initializeVideos(session, cameraView, directoryName, startTime = None, datetimeFormat = None): + '''Initializes videos with time or tries to guess it from filename + directoryName should contain the videos to find and be the relative path from the site location''' + names = sorted(listdir(directoryName)) + videoSequences = [] + if datetimeFormat is not None: + timeConverter = TimeConverter(datetimeFormat) + for name in names: + prefix = removeExtension(name) + extension = getExtension(name) + if extension in videoFilenameExtensions: + if datetimeFormat is not None: + from argparse import ArgumentTypeError + try: + t1 = timeConverter.convert(name[:name.rfind('_')]) + print('DB time {} / Time from filename {}'.format(startTime, t1)) + except ArgumentTypeError as e: + print('File format error for time {} (prefix {})'.format(name, prefix)) + vidinfo = infoVideo(directoryName+sep+name) + duration = vidinfo['number of frames']#timedelta(minutes = 27, seconds = 33) + fps = vidinfo['fps'] + duration = timedelta(seconds=duration/fps) + videoSequences.append(VideoSequence(directoryName+sep+name, startTime, duration, cameraView, directoryName+sep+prefix+'.sqlite')) + startTime += duration + session.add_all(videoSequences) + session.commit() + +# management +# TODO need to be able to copy everything from a site from one sqlite to another, and delete everything attached to a site