├── .gitignore ├── CHANGELOG.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── datasets └── nordland │ ├── Nordlandsbanen full HD.url │ └── getDataset.bash ├── pyseqslam ├── __init__.py ├── demo.py ├── parameters.py ├── seqslam.py └── utils.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | /*.egg 4 | /*.egg-info 5 | /**/__pycache__/ 6 | *.pyc 7 | *.py~ 8 | /auto 9 | *.project 10 | *.pydevproject 11 | *.yml 12 | /.settings 13 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Version 0.1 2 | ----------- 3 | 4 | - Initial version. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is a Python port of Niko Sünderhauf's OpenSeqSLAM, whose original code can be found here: 2 | http://www.tu-chemnitz.de/etit/proaut/mitarbeiter/niko.html 3 | 4 | 5 | OpenSeqSLAM 6 | =========== 7 | 8 | Copyright 2013, Niko Sünderhauf 9 | Chemnitz University of Technology 10 | niko@etit.tu-chemnitz.de 11 | 12 | OpenSeqSLAM is an open source Matlab implementation of the original SeqSLAM 13 | algorithm published by Milford and Wyeth at ICRA12 [1]. SeqSLAM performs place 14 | recognition by matching sequences of images. 15 | 16 | Quick start guide: 17 | - Download the Nordland dataset: 18 | cd datasets/norland; ./getDataset.bash; 19 | - start Matlab and run demo.m from within the matlab directory 20 | 21 | 22 | [1] Michael Milford and Gordon F. Wyeth (2012). SeqSLAM: Visual Route-Based Navigation for Sunny Summer Days and Stormy Winter Nights. In Proc. of IEEE Intl. Conf. on Robotics and Automation (ICRA) 23 | 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHANGELOG.txt LICENSE.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a Python port of Niko Sünderhauf's [OpenSeqSLAM code](http://www.tu-chemnitz.de/etit/proaut/mitarbeiter/niko.html). 2 | 3 | SeqSLAM performs place recognition by matching sequences of images. 4 | 5 | Quick start: 6 | - Download the Nordland dataset: 7 | 8 | ```cd datasets/norland; ./getDataset.bash; ``` 9 | 10 | - Run demo: 11 | 12 | ``` 13 | cd pyseqslam 14 | python demo.py 15 | ``` 16 | 17 | (This will match the spring sequence of the nordland dataset against the winter sequence. To change which datasets are used, set the environment variables DATASET_1_PATH and DATASET_2_PATH) 18 | 19 | [1] Michael Milford and Gordon F. Wyeth (2012). SeqSLAM: Visual Route-Based Navigation for Sunny Summer Days and Stormy Winter Nights. In Proc. of IEEE Intl. Conf. on Robotics and Automation (ICRA) 20 | -------------------------------------------------------------------------------- /datasets/nordland/Nordlandsbanen full HD.url: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmadl/pySeqSLAM/56dee085063aaa455da8488e8211c66b23597ac4/datasets/nordland/Nordlandsbanen full HD.url -------------------------------------------------------------------------------- /datasets/nordland/getDataset.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Downloading dataset ..." 3 | wget http://www.tu-chemnitz.de/etit/proaut/datasets/nordland/64x32-grayscale-1fps.tar 4 | 5 | echo "Extracting dataset ..." 6 | tar xf 64x32-grayscale-1fps.tar 7 | -------------------------------------------------------------------------------- /pyseqslam/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | pySeqSLAM: Main module 3 | 4 | Copyright 2015, Tamas Madl 5 | Licensed under MIT. 6 | ''' 7 | 8 | 9 | def main(): 10 | ''' 11 | Main function of the boilerplate code is the entry point of the 'pyseqslam' executable script (defined in setup.py). 12 | 13 | Use doctests, those are very helpful. 14 | 15 | >>> main() 16 | Hello 17 | >>> 2 + 2 18 | 4 19 | ''' 20 | 21 | print("Hello") 22 | 23 | -------------------------------------------------------------------------------- /pyseqslam/demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | """ 3 | OpenSeqSLAM 4 | Copyright 2013, Niko S��nderhauf Chemnitz University of Technology niko@etit.tu-chemnitz.de 5 | 6 | pySeqSLAM is an open source Python implementation of the original SeqSLAM algorithm published by Milford and Wyeth at ICRA12 [1]. SeqSLAM performs place recognition by matching sequences of images. 7 | 8 | [1] Michael Milford and Gordon F. Wyeth (2012). SeqSLAM: Visual Route-Based Navigation for Sunny Summer Days and Stormy Winter Nights. In Proc. of IEEE Intl. Conf. on Robotics and Automation (ICRA) 9 | """ 10 | from parameters import defaultParameters 11 | from utils import AttributeDict 12 | import matplotlib 13 | import matplotlib.pyplot as plt 14 | from copy import deepcopy 15 | import time 16 | import os 17 | 18 | from seqslam import * 19 | 20 | def demo(): 21 | 22 | # set the parameters 23 | 24 | # start with default parameters 25 | params = defaultParameters() 26 | 27 | # Nordland spring dataset 28 | ds = AttributeDict() 29 | ds.name = 'spring' 30 | 31 | try: 32 | path = os.environ['DATASET_1_PATH'] 33 | except: 34 | path = '../datasets/nordland/64x32-grayscale-1fps/spring' 35 | print "Warning: Environment variable DATASET_1_PATH not found! Trying '"+path+"'" 36 | ds.imagePath = path 37 | 38 | ds.prefix='images-' 39 | ds.extension='.png' 40 | ds.suffix='' 41 | ds.imageSkip = 100 # use every n-nth image 42 | ds.imageIndices = range(1, 35700, ds.imageSkip) 43 | ds.savePath = 'results' 44 | ds.saveFile = '%s-%d-%d-%d' % (ds.name, ds.imageIndices[0], ds.imageSkip, ds.imageIndices[-1]) 45 | 46 | ds.preprocessing = AttributeDict() 47 | ds.preprocessing.save = 1 48 | ds.preprocessing.load = 0 #1 49 | #ds.crop=[1 1 60 32] # x0 y0 x1 y1 cropping will be done AFTER resizing! 50 | ds.crop=[] 51 | 52 | spring=ds 53 | 54 | ds2 = deepcopy(ds) 55 | # Nordland winter dataset 56 | ds2.name = 'winter' 57 | #ds.imagePath = '../datasets/nordland/64x32-grayscale-1fps/winter' 58 | try: 59 | path = os.environ['DATASET_2_PATH'] 60 | except: 61 | path = '../datasets/nordland/64x32-grayscale-1fps/winter' 62 | print "Warning: Environment variable DATASET_2_PATH not found! Trying '"+path+"'" 63 | ds2.saveFile = '%s-%d-%d-%d' % (ds2.name, ds2.imageIndices[0], ds2.imageSkip, ds2.imageIndices[-1]) 64 | # ds.crop=[5 1 64 32] 65 | ds2.crop=[] 66 | 67 | winter=ds2 68 | 69 | params.dataset = [spring, winter] 70 | 71 | # load old results or re-calculate? 72 | params.differenceMatrix.load = 0 73 | params.contrastEnhanced.load = 0 74 | params.matching.load = 0 75 | 76 | # where to save / load the results 77 | params.savePath='results' 78 | 79 | ## now process the dataset 80 | ss = SeqSLAM(params) 81 | t1=time.time() 82 | results = ss.run() 83 | t2=time.time() 84 | print "time taken: "+str(t2-t1) 85 | 86 | ## show some results 87 | if len(results.matches) > 0: 88 | m = results.matches[:,0] # The LARGER the score, the WEAKER the match. 89 | thresh=0.9 # you can calculate a precision-recall plot by varying this threshold 90 | m[results.matches[:,1]>thresh] = np.nan # remove the weakest matches 91 | plt.plot(m,'.') # ideally, this would only be the diagonal 92 | plt.title('Matchings') 93 | plt.show() 94 | else: 95 | print "Zero matches" 96 | 97 | 98 | if __name__ == "__main__": 99 | demo() 100 | -------------------------------------------------------------------------------- /pyseqslam/parameters.py: -------------------------------------------------------------------------------- 1 | from utils import AttributeDict 2 | from PIL import Image 3 | 4 | def defaultParameters(): 5 | 6 | params = AttributeDict() 7 | 8 | # switches 9 | params.DO_PREPROCESSING = 1 10 | params.DO_RESIZE = 0 11 | params.DO_GRAYLEVEL = 1 12 | params.DO_PATCHNORMALIZATION = 1 #!!!! 1 13 | params.DO_SAVE_PREPROCESSED_IMG = 0 14 | params.DO_DIFF_MATRIX = 1 15 | params.DO_CONTRAST_ENHANCEMENT = 1 16 | params.DO_FIND_MATCHES = 1 17 | 18 | 19 | # parameters for preprocessing 20 | params.downsample = AttributeDict() 21 | params.downsample.size = [32, 64] # height, width 22 | try: 23 | params.downsample.method = Image.LANCZOS 24 | except: 25 | params.downsample.method = Image.ANTIALIAS 26 | params.normalization = AttributeDict() 27 | params.normalization.sideLength = 8 28 | params.normalization.mode = 1 29 | 30 | 31 | # parameters regarding the matching between images 32 | params.matching = AttributeDict() 33 | params.matching.ds = 10 34 | params.matching.Rrecent=5 35 | params.matching.vmin = 0.8 36 | params.matching.vskip = 0.1 37 | params.matching.vmax = 1.2 38 | params.matching.Rwindow = 10 39 | params.matching.save = 1 40 | params.matching.load = 0 #1 41 | 42 | # parameters for contrast enhancement on difference matrix 43 | params.contrastEnhancement = AttributeDict() 44 | params.contrastEnhancement.R = 10 45 | 46 | # load old results or re-calculate? save results? 47 | params.differenceMatrix = AttributeDict() 48 | params.differenceMatrix.save = 1 49 | params.differenceMatrix.load = 0 #1 50 | 51 | params.contrastEnhanced = AttributeDict() 52 | params.contrastEnhanced.save = 1 53 | params.contrastEnhanced.load = 0 #1 54 | 55 | # suffix appended on files containing the results 56 | params.saveSuffix='' 57 | 58 | return params 59 | -------------------------------------------------------------------------------- /pyseqslam/seqslam.py: -------------------------------------------------------------------------------- 1 | from utils import AttributeDict 2 | import os 3 | import numpy as np 4 | from scipy.io import loadmat, savemat 5 | import matplotlib.image as mpimg 6 | from PIL import Image 7 | from copy import deepcopy 8 | 9 | import matplotlib 10 | import matplotlib.pyplot as plt 11 | import matplotlib.cm as cm 12 | 13 | class SeqSLAM(): 14 | params = None 15 | 16 | def __init__(self, params): 17 | self.params = params 18 | 19 | def run(self): 20 | # begin with preprocessing of the images 21 | if self.params.DO_PREPROCESSING: 22 | results = self.doPreprocessing() 23 | 24 | # image difference matrix 25 | if self.params.DO_DIFF_MATRIX: 26 | results = self.doDifferenceMatrix(results) 27 | 28 | # contrast enhancement 29 | if self.params.DO_CONTRAST_ENHANCEMENT: 30 | results = self.doContrastEnhancement(results) 31 | else: 32 | if self.params.DO_DIFF_MATRIX: 33 | results.DD = results.D 34 | 35 | # find the matches 36 | if self.params.DO_FIND_MATCHES: 37 | results = self.doFindMatches(results) 38 | return results 39 | 40 | def doPreprocessing(self): 41 | results = AttributeDict() 42 | results.dataset = [] 43 | for i in range(len(self.params.dataset)): 44 | # shall we just load it? 45 | filename = '%s/preprocessing-%s%s.mat' % (self.params.dataset[i].savePath, self.params.dataset[i].saveFile, self.params.saveSuffix) 46 | if self.params.dataset[i].preprocessing.load and os.path.isfile(filename): 47 | r = loadmat(filename) 48 | print('Loading file %s ...' % filename) 49 | results.dataset[i].preprocessing = r.results_preprocessing 50 | else: 51 | # or shall we actually calculate it? 52 | p = deepcopy(self.params) 53 | p.dataset = self.params.dataset[i] 54 | d = AttributeDict() 55 | d.preprocessing = np.copy(SeqSLAM.preprocessing(p)) 56 | results.dataset.append(d) 57 | 58 | if self.params.dataset[i].preprocessing.save: 59 | results_preprocessing = results.dataset[i].preprocessing 60 | savemat(filename, {'results_preprocessing': results_preprocessing}) 61 | 62 | return results 63 | 64 | @staticmethod 65 | def rgb2gray(rgb): 66 | r, g, b = rgb[:,:,0], rgb[:,:,1], rgb[:,:,2] 67 | gray = 0.2989 * r + 0.5870 * g + 0.1140 * b 68 | return gray 69 | 70 | @staticmethod 71 | def preprocessing(params): 72 | print('Preprocessing dataset %s, indices %d - %d ...' % (params.dataset.name, params.dataset.imageIndices[0], params.dataset.imageIndices[-1])) 73 | # allocate memory for all the processed images 74 | n = len(params.dataset.imageIndices) 75 | m = params.downsample.size[0]*params.downsample.size[1] 76 | 77 | if len(params.dataset.crop) > 0: 78 | c = params.dataset.crop 79 | m = (c[2]-c[0]+1) * (c[3]-c[1]+1) 80 | 81 | images = np.zeros((m,n), 'uint8') 82 | j=0 83 | 84 | # for every image .... 85 | for i in (params.dataset.imageIndices): 86 | filename = '%s/%s%05d%s%s' % (params.dataset.imagePath, \ 87 | params.dataset.prefix, \ 88 | i, \ 89 | params.dataset.suffix, \ 90 | params.dataset.extension) 91 | 92 | img = Image.open(filename) 93 | 94 | # convert to grayscale 95 | if params.DO_GRAYLEVEL: 96 | #img = img.convert('L') #LA to include alpha 97 | img = SeqSLAM.rgb2gray(np.asarray(img)) 98 | 99 | # resize the image 100 | if params.DO_RESIZE: 101 | img = img.resize(params.downsample.size, params.downsample.method) 102 | 103 | img = np.copy(np.asarray(img)) 104 | #img.flags.writeable = True 105 | 106 | # crop the image if necessary 107 | if len(params.dataset.crop) > 0: 108 | img = img[params.dataset.crop[1]:params.dataset.crop[3], params.dataset.crop[0]:params.dataset.crop[2]] 109 | 110 | # do patch normalization 111 | if params.DO_PATCHNORMALIZATION: 112 | img = SeqSLAM.patchNormalize(img, params) 113 | 114 | # shall we save the result? 115 | if params.DO_SAVE_PREPROCESSED_IMG: 116 | pass 117 | 118 | images[:,j] = img.flatten(0) 119 | j += 1 120 | 121 | return images 122 | 123 | 124 | @staticmethod 125 | def patchNormalize(img, params): 126 | s = params.normalization.sideLength 127 | 128 | n = range(0, img.shape[0]+2, s) 129 | m = range(0, img.shape[1]+2, s) 130 | 131 | for i in range(len(n)-1): 132 | for j in range(len(m)-1): 133 | p = img[n[i]:n[i+1], m[j]:m[j+1]] 134 | 135 | pp=np.copy(p.flatten(1)) 136 | 137 | if params.normalization.mode != 0: 138 | pp=pp.astype(float) 139 | img[n[i]:n[i+1], m[j]:m[j+1]] = 127+np.reshape(np.round((pp-np.mean(pp))/np.std(pp, ddof=1)), (s, s)) 140 | else: 141 | f = 255.0/np.max((1, np.max(pp) - np.min(pp))) 142 | img[n[i]:n[i+1], m[j]:m[j+1]] = np.round(f * (p-np.min(pp))) 143 | 144 | #print str((n[i], n[i+1], m[j], m[j+1])) 145 | return img 146 | 147 | def getDifferenceMatrix(self, data0preproc, data1preproc): 148 | # TODO parallelize 149 | n = data0preproc.shape[1] 150 | m = data1preproc.shape[1] 151 | D = np.zeros((n, m)) 152 | 153 | #parfor? 154 | for i in range(n): 155 | d = data1preproc - np.tile(data0preproc[:,i],(m, 1)).T 156 | D[i,:] = np.sum(np.abs(d), 0)/n 157 | 158 | return D 159 | 160 | def doDifferenceMatrix(self, results): 161 | filename = '%s/difference-%s-%s%s.mat' % (self.params.savePath, self.params.dataset[0].saveFile, self.params.dataset[1].saveFile, self.params.saveSuffix) 162 | 163 | if self.params.differenceMatrix.load and os.path.isfile(filename): 164 | print('Loading image difference matrix from file %s ...' % filename) 165 | 166 | d = loadmat(filename) 167 | results.D = d.D 168 | else: 169 | if len(results.dataset)<2: 170 | print('Error: Cannot calculate difference matrix with less than 2 datasets.') 171 | return None 172 | 173 | print('Calculating image difference matrix ...') 174 | 175 | results.D=self.getDifferenceMatrix(results.dataset[0].preprocessing, results.dataset[1].preprocessing) 176 | 177 | # save it 178 | if self.params.differenceMatrix.save: 179 | savemat(filename, {'D':results.D}) 180 | 181 | return results 182 | 183 | def enhanceContrast(self, D): 184 | # TODO parallelize 185 | DD = np.zeros(D.shape) 186 | 187 | #parfor? 188 | for i in range(D.shape[0]): 189 | a=np.max((0, i-self.params.contrastEnhancement.R/2)) 190 | b=np.min((D.shape[0], i+self.params.contrastEnhancement.R/2+1)) 191 | v = D[a:b, :] 192 | DD[i,:] = (D[i,:] - np.mean(v, 0)) / np.std(v, 0, ddof=1) 193 | 194 | return DD-np.min(np.min(DD)) 195 | 196 | def doContrastEnhancement(self, results): 197 | 198 | filename = '%s/differenceEnhanced-%s-%s%s.mat' % (self.params.savePath, self.params.dataset[0].saveFile, self.params.dataset[1].saveFile, self.params.saveSuffix) 199 | 200 | if self.params.contrastEnhanced.load and os.path.isfile(filename): 201 | print('Loading contrast-enhanced image distance matrix from file %s ...' % filename) 202 | dd = loadmat(filename) 203 | results.DD = dd.DD 204 | else: 205 | print('Performing local contrast enhancement on difference matrix ...') 206 | 207 | # let the minimum distance be 0 208 | results.DD = self.enhanceContrast(results.D) 209 | 210 | # save it? 211 | if self.params.contrastEnhanced.save: 212 | DD = results.DD 213 | savemat(filename, {'DD':DD}) 214 | 215 | return results 216 | 217 | def doFindMatches(self, results): 218 | 219 | filename = '%s/matches-%s-%s%s.mat' % (self.params.savePath, self.params.dataset[0].saveFile, self.params.dataset[1].saveFile, self.params.saveSuffix) 220 | 221 | if self.params.matching.load and os.path.isfile(filename): 222 | print('Loading matchings from file %s ...' % filename) 223 | m = loadmat(filename) 224 | results.matches = m.matches 225 | else: 226 | 227 | print('Searching for matching images ...') 228 | 229 | # make sure ds is dividable by two 230 | self.params.matching.ds = self.params.matching.ds + np.mod(self.params.matching.ds,2) 231 | 232 | matches = self.getMatches(results.DD) 233 | 234 | # save it 235 | if self.params.matching.save: 236 | savemat(filename, {'matches':matches}) 237 | 238 | results.matches = matches 239 | 240 | return results 241 | 242 | def getMatches(self, DD): 243 | # TODO parallelize 244 | matches = np.nan*np.ones((DD.shape[1],2)) 245 | # parfor? 246 | for N in range(self.params.matching.ds/2, DD.shape[1]-self.params.matching.ds/2): 247 | # find a single match 248 | 249 | # We shall search for matches using velocities between 250 | # params.matching.vmin and params.matching.vmax. 251 | # However, not every vskip may be neccessary to check. So we first find 252 | # out, which v leads to different trajectories: 253 | 254 | move_min = self.params.matching.vmin * self.params.matching.ds 255 | move_max = self.params.matching.vmax * self.params.matching.ds 256 | 257 | move = np.arange(int(move_min), int(move_max)+1) 258 | v = move.astype(float) / self.params.matching.ds 259 | 260 | idx_add = np.tile(np.arange(0, self.params.matching.ds+1), (len(v),1)) 261 | idx_add = np.floor(idx_add * np.tile(v, (idx_add.shape[1], 1)).T) 262 | 263 | # this is where our trajectory starts 264 | n_start = N + 1 - self.params.matching.ds/2 265 | x= np.tile(np.arange(n_start , n_start+self.params.matching.ds+1), (len(v), 1)) 266 | 267 | #TODO idx_add and x now equivalent to MATLAB, dh 1 indexing 268 | score = np.zeros(DD.shape[0]) 269 | 270 | # add a line of inf costs so that we penalize running out of data 271 | DD=np.vstack((DD, np.infty*np.ones((1,DD.shape[1])))) 272 | 273 | y_max = DD.shape[0] 274 | xx = (x-1) * y_max 275 | 276 | flatDD = DD.flatten(1) 277 | for s in range(1, DD.shape[0]): 278 | y = np.copy(idx_add+s) 279 | y[y>y_max]=y_max 280 | idx = (xx + y).astype(int) 281 | ds = np.sum(flatDD[idx-1],1) 282 | score[s-1] = np.min(ds) 283 | 284 | 285 | # find min score and 2nd smallest score outside of a window 286 | # around the minimum 287 | 288 | min_idx = np.argmin(score) 289 | min_value=score[min_idx] 290 | window = np.arange(np.max((0, min_idx-self.params.matching.Rwindow/2)), np.min((len(score), min_idx+self.params.matching.Rwindow/2))) 291 | not_window = list(set(range(len(score))).symmetric_difference(set(window))) #xor 292 | min_value_2nd = np.min(score[not_window]) 293 | 294 | match = [min_idx + self.params.matching.ds/2, min_value / min_value_2nd] 295 | matches[N,:] = match 296 | 297 | return matches 298 | 299 | -------------------------------------------------------------------------------- /pyseqslam/utils.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | class AttributeDict(dict): 4 | def __getattr__(self, attr): 5 | return self[attr] 6 | def __setattr__(self, attr, value): 7 | self[attr] = value 8 | def __copy__(self): 9 | cls = self.__class__ 10 | result = cls.__new__(cls) 11 | result.__dict__.update(self.__dict__) 12 | return result 13 | def __deepcopy__(self, memo): 14 | cls = self.__class__ 15 | result = cls.__new__(cls) 16 | memo[id(self)] = result 17 | for k, v in self.items(): 18 | setattr(result, k, deepcopy(v, memo)) 19 | return result 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | tag_svn_revision = false 4 | 5 | [pytest] 6 | addopts = --ignore=setup.py --ignore=build --ignore=dist --doctest-modules 7 | norecursedirs=*.egg 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Python implementation of SeqSLAM by Michael Milford. Performs place recognition by matching sequences of images. 3 | ''' 4 | import sys 5 | from setuptools import setup, find_packages 6 | from setuptools.command.test import test as TestCommand 7 | 8 | # This is a plug-in for setuptools that will invoke py.test 9 | # when you run python setup.py test 10 | class PyTest(TestCommand): 11 | def finalize_options(self): 12 | TestCommand.finalize_options(self) 13 | self.test_args = [] 14 | self.test_suite = True 15 | 16 | def run_tests(self): 17 | import pytest # import here, because outside the required eggs aren't loaded yet 18 | sys.exit(pytest.main(self.test_args)) 19 | 20 | 21 | version = "0.1" 22 | 23 | setup(name="pySeqSLAM", 24 | version=version, 25 | description="Python implementation of SeqSLAM by Michael Milford. Performs place recognition by matching sequences of images.", 26 | long_description=open("README.md").read(), 27 | classifiers=[ # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers 28 | 'Development Status :: 1 - Planning', 29 | 'Programming Language :: Python' 30 | ], 31 | keywords="place recognition, SeqSLAM", # Separate with spaces 32 | author="", 33 | author_email="", 34 | url="", 35 | license="MIT", 36 | packages=find_packages(exclude=['examples', 'tests']), 37 | include_package_data=True, 38 | zip_safe=True, 39 | tests_require=['pytest'], 40 | cmdclass={'test': PyTest}, 41 | 42 | # TODO: List of packages that this one depends upon: 43 | install_requires=['scipy', 'numpy', 'matplotlib', 'Pillow'], 44 | # TODO: List executable scripts, provided by the package (this is just an example) 45 | entry_points={ 46 | 'console_scripts': 47 | ['pySeqSLAM=pyseqslam:main'] 48 | } 49 | ) 50 | --------------------------------------------------------------------------------