diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..eaca935
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,16 @@
+module.exports = function(grunt) {
+
+ require('time-grunt')(grunt);
+
+ grunt.initConfig({
+ jshint: {
+ options: {
+ jshintrc: ".jshintrc"
+ },
+ all: ['Gruntfile.js', 'libs/**/*.js', 'docs/**/*.js', 'index.js', 'config.js']
+ }
+ });
+
+ grunt.loadNpmTasks('grunt-contrib-jshint');
+ grunt.registerTask('default', ['jshint']);
+};
\ No newline at end of file
diff --git a/README.md b/README.md
index 3fed387..a93f8e4 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,16 @@ http://www.buildsucceeded.com/2015/solved-pm2-startup-at-boot-time-centos-7-red-
You can monitor the process using `pm2 status`.
+### Test Mode
+
+If you want to make a contribution, but don't want to setup OpenDroneMap, or perhaps you are working on a Windows machine, or if you want to run automated tests, you can turn test mode on:
+
+```
+node index.js --test
+```
+
+While in test mode all calls to OpenDroneMap's code will be simulated (see the /tests directory for the mock data that is returned).
+
### Test Images
You can find some test drone images [here](https://github.com/dakotabenjamin/odm_data).
diff --git a/index.js b/index.js
index 106b37e..87136dd 100644
--- a/index.js
+++ b/index.js
@@ -36,6 +36,7 @@ let morgan = require('morgan');
let TaskManager = require('./libs/TaskManager');
let Task = require('./libs/Task');
let odmOptions = require('./libs/odmOptions');
+let Directories = require('./libs/Directories');
let winstonStream = {
write: function(message, encoding){
@@ -51,14 +52,14 @@ app.use('/swagger.json', express.static('docs/swagger.json'));
let upload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => {
- let path = `tmp/${req.id}/`;
- fs.exists(path, exists => {
+ let dstPath = path.join("tmp", req.id);
+ fs.exists(dstPath, exists => {
if (!exists){
- fs.mkdir(path, undefined, () => {
- cb(null, path);
+ fs.mkdir(dstPath, undefined, () => {
+ cb(null, dstPath);
});
}else{
- cb(null, path);
+ cb(null, dstPath);
}
});
},
@@ -115,10 +116,10 @@ let server;
app.post('/task/new', addRequestId, upload.array('images'), (req, res) => {
if (req.files.length === 0) res.json({error: "Need at least 1 file."});
else{
- let srcPath = `tmp/${req.id}`;
- let destPath = `data/${req.id}`;
- let destImagesPath = `${destPath}/images`;
- let destGpcPath = `${destPath}/gpc`;
+ let srcPath = path.join("tmp", req.id);
+ let destPath = path.join(Directories.data, req.id);
+ let destImagesPath = path.join(destPath, "images");
+ let destGpcPath = path.join(destPath, "gpc");
async.series([
cb => {
diff --git a/libs/Directories.js b/libs/Directories.js
new file mode 100644
index 0000000..92e2af7
--- /dev/null
+++ b/libs/Directories.js
@@ -0,0 +1,28 @@
+/*
+Node-OpenDroneMap Node.js App and REST API to access OpenDroneMap.
+Copyright (C) 2016 Node-OpenDroneMap Contributors
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+"use strict";
+let config = require('../config');
+let path = require('path');
+
+class Directories{
+ static get data(){
+ return !config.test ? "data" : path.join("tests", "data");
+ }
+}
+
+module.exports = Directories;
\ No newline at end of file
diff --git a/libs/Task.js b/libs/Task.js
index 27d08e7..d86e26a 100644
--- a/libs/Task.js
+++ b/libs/Task.js
@@ -17,6 +17,7 @@ along with this program. If not, see .
*/
"use strict";
+let config = require('../config');
let async = require('async');
let assert = require('assert');
let logger = require('./logger');
@@ -26,6 +27,7 @@ let rmdir = require('rimraf');
let odmRunner = require('./odmRunner');
let archiver = require('archiver');
let os = require('os');
+let Directories = require('./Directories');
let statusCodes = require('./statusCodes');
@@ -99,25 +101,25 @@ module.exports = class Task{
// Get path where images are stored for this task
// (relative to nodejs process CWD)
getImagesFolderPath(){
- return `${this.getProjectFolderPath()}/images`;
+ return path.join(this.getProjectFolderPath(), "images");
}
// Get path where GPC file(s) are stored
// (relative to nodejs process CWD)
getGpcFolderPath(){
- return `${this.getProjectFolderPath()}/gpc`;
+ return path.join(this.getProjectFolderPath(), "gpc");
}
// Get path of project (where all images and assets folder are contained)
// (relative to nodejs process CWD)
getProjectFolderPath(){
- return `data/${this.uuid}`;
+ return path.join(Directories.data, this.uuid);
}
// Get the path of the archive where all assets
// outputted by this task are stored.
getAssetsArchivePath(){
- return `${this.getProjectFolderPath()}/all.zip`;
+ return path.join(this.getProjectFolderPath(), "all.zip");
}
// Deletes files and folders related to this task
@@ -207,16 +209,21 @@ module.exports = class Task{
archive.on('error', err => {
this.setStatus(statusCodes.FAILED);
+ logger.error(`Could not archive .zip file: ${err.message}`);
finished(err);
});
archive.pipe(output);
- archive
- .directory(`${this.getProjectFolderPath()}/odm_orthophoto`, 'odm_orthophoto')
- .directory(`${this.getProjectFolderPath()}/odm_georeferencing`, 'odm_georeferencing')
- .directory(`${this.getProjectFolderPath()}/odm_texturing`, 'odm_texturing')
- .directory(`${this.getProjectFolderPath()}/odm_meshing`, 'odm_meshing')
- .finalize();
+ ['odm_orthophoto', 'odm_georeferencing', 'odm_texturing', 'odm_meshing'].forEach(folderToArchive => {
+ let sourcePath = !config.test ?
+ this.getProjectFolderPath() :
+ path.join("tests", "processing_results");
+
+ archive.directory(
+ path.join(sourcePath, folderToArchive),
+ folderToArchive);
+ });
+ archive.finalize();
};
if (this.status.code === statusCodes.QUEUED){
diff --git a/libs/TaskManager.js b/libs/TaskManager.js
index dfdd333..d55f1fb 100644
--- a/libs/TaskManager.js
+++ b/libs/TaskManager.js
@@ -26,9 +26,9 @@ let Task = require('./Task');
let statusCodes = require('./statusCodes');
let async = require('async');
let schedule = require('node-schedule');
+let Directories = require('./Directories');
-const DATA_DIR = "data";
-const TASKS_DUMP_FILE = `${DATA_DIR}/tasks.json`;
+const TASKS_DUMP_FILE = path.join(Directories.data, "tasks.json");
const CLEANUP_TASKS_IF_OLDER_THAN = 1000 * 60 * 60 * 24 * config.cleanupTasksAfter; // days
module.exports = class TaskManager{
@@ -85,11 +85,11 @@ module.exports = class TaskManager{
removeOrphanedDirectories(done){
logger.info("Checking for orphaned directories to be removed...");
- fs.readdir(DATA_DIR, (err, entries) => {
+ fs.readdir(Directories.data, (err, entries) => {
if (err) done(err);
else{
async.eachSeries(entries, (entry, cb) => {
- let dirPath = path.join(DATA_DIR, entry);
+ let dirPath = path.join(Directories.data, entry);
if (fs.statSync(dirPath).isDirectory() &&
entry.match(/^[\w\d]+\-[\w\d]+\-[\w\d]+\-[\w\d]+\-[\w\d]+$/) &&
!this.tasks[entry]){
diff --git a/libs/logger.js b/libs/logger.js
index 5aba9bf..5f5aad8 100644
--- a/libs/logger.js
+++ b/libs/logger.js
@@ -26,7 +26,7 @@ let path = require('path');
// Configure custom File transport to write plain text messages
let logPath = ( config.logger.logDirectory ?
config.logger.logDirectory :
- `${__dirname}/../` );
+ path.join(__dirname, "..") );
// Check that log file directory can be written to
try {
diff --git a/libs/odmOptions.js b/libs/odmOptions.js
index 3319283..af3a263 100644
--- a/libs/odmOptions.js
+++ b/libs/odmOptions.js
@@ -189,6 +189,7 @@ module.exports = {
// Scan through all possible options
for (let odmOption of odmOptions){
// Was this option selected by the user?
+ /*jshint loopfunc: true */
let opt = options.find(o => o.name === odmOption.name);
if (opt){
try{
diff --git a/libs/odmRunner.js b/libs/odmRunner.js
index e20a4f2..f17367f 100644
--- a/libs/odmRunner.js
+++ b/libs/odmRunner.js
@@ -28,7 +28,7 @@ module.exports = {
run: function(options, done, outputReceived){
assert(options["project-path"] !== undefined, "project-path must be defined");
- let command = [`${config.odm_path}/run.py`];
+ let command = [path.join(config.odm_path, "run.py")];
for (var name in options){
let value = options[name];
@@ -48,8 +48,18 @@ module.exports = {
if (config.test){
logger.info("Test mode is on, command will not execute");
- // TODO: simulate test output
- done(null, 0, null);
+ let outputTestFile = path.join("..", "tests", "odm_output.txt");
+ fs.readFile(path.resolve(__dirname, outputTestFile), 'utf8', (err, text) => {
+ if (!err){
+ let lines = text.split("\n");
+ lines.forEach(line => outputReceived(line));
+
+ done(null, 0, null);
+ }else{
+ logger.warn(`Error: ${err.message}`);
+ done(err);
+ }
+ });
return; // Skip rest
}
@@ -71,22 +81,18 @@ module.exports = {
// In test mode, we don't call ODM,
// instead we return a mock
if (config.test){
- let optionsTestFile = "../tests/odm_options.json";
+ let optionsTestFile = path.join("..", "tests", "odm_options.json");
fs.readFile(path.resolve(__dirname, optionsTestFile), 'utf8', (err, json) => {
if (!err){
try{
let options = JSON.parse(json);
-
- // We also mark each description with "TEST" (to make sure we know this is not real data)
- options.forEach(option => { option.help = "## TEST ##" + (option.help !== undefined ? ` ${option.help}` : ""); });
-
done(null, options);
}catch(e){
- console.log(`Invalid test options ${optionsTestFile}: ${err.message}`);
+ logger.warn(`Invalid test options ${optionsTestFile}: ${err.message}`);
done(e);
}
}else{
- console.log(`Error: ${err.message}`);
+ logger.warn(`Error: ${err.message}`);
done(err);
}
});
@@ -95,7 +101,7 @@ module.exports = {
}
// Launch
- let childProcess = spawn("python", [`${__dirname}/../helpers/odmOptionsToJson.py`,
+ let childProcess = spawn("python", [path.join(__dirname, "..", "helpers", "odmOptionsToJson.py"),
"--project-path", config.odm_path]);
let output = [];
diff --git a/package.json b/package.json
index 0ef57db..dc0dda7 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,9 @@
"winston": "^2.2.0"
},
"devDependencies": {
- "nodemon": "^1.9.2"
+ "grunt": "^1.0.1",
+ "grunt-contrib-jshint": "^1.0.0",
+ "nodemon": "^1.9.2",
+ "time-grunt": "^1.4.0"
}
}
diff --git a/public/js/fileinput.js b/public/js/fileinput.js
index 7542b38..f8c2578 100644
--- a/public/js/fileinput.js
+++ b/public/js/fileinput.js
@@ -2216,9 +2216,9 @@
}
if (!self.showPreview) {
self.addToStack(file);
- setTimeout(function () {
+ // setTimeout(function () {
readFile(i + 1);
- }, 100);
+ // }, 100);
self._raise('fileloaded', [file, previewId, i, reader]);
return;
}
diff --git a/public/js/main.js b/public/js/main.js
index 1335f2a..87f5971 100644
--- a/public/js/main.js
+++ b/public/js/main.js
@@ -302,8 +302,7 @@ $(function(){
$("#btnUpload").removeAttr('disabled')
.val(btnUploadLabel);
})
- .on('filebatchuploaderror', function(e, data, msg){
- });
+ .on('filebatchuploaderror', console.warn);
// Load options
function Option(properties){
diff --git a/tests/odm_options.json b/tests/odm_options.json
index 19142a2..5fcae67 100644
--- a/tests/odm_options.json
+++ b/tests/odm_options.json
@@ -1,205 +1 @@
-[
- {
- "domain": "integer",
- "help": "The maximum number of images per cluster. Default: 500",
- "name": "cmvs-maxImages",
- "type": "int",
- "value": "500"
- },
- {
- "domain": "positive integer",
- "help": "Oct-tree depth used in the mesh reconstruction, increase to get more vertices, recommended values are 8-12. Default: 9",
- "name": "odm_meshing-octreeDepth",
- "type": "int",
- "value": "9"
- },
- {
- "domain": "positive integer",
- "help": "The maximum vertex count of the output mesh Default: 100000",
- "name": "odm_meshing-maxVertexCount",
- "type": "int",
- "value": "100000"
- },
- {
- "domain": "percent",
- "help": "Ignore matched keypoints if the two images share less than percent of keypoints. Default: 2",
- "name": "matcher-threshold",
- "type": "float",
- "value": "2"
- },
- {
- "domain": "integer",
- "help": "Minimum number of features to extract per image. More features leads to better results but slower execution. Default: 4000",
- "name": "min-num-features",
- "type": "int",
- "value": "4000"
- },
- {
- "domain": "positive float",
- "help": "Override the focal length information for the images",
- "name": "force-focal",
- "type": "float",
- "value": "0"
- },
- {
- "domain": "integer",
- "help": "resizes images by the largest side",
- "name": "resize-to",
- "type": "int",
- "value": "2400"
- },
- {
- "domain": "positive integer",
- "help": "The level in the image pyramid that is used for the computation. see http://www.di.ens.fr/pmvs/documentation.html for more pmvs documentation. Default: 1",
- "name": "pmvs-level",
- "type": "int",
- "value": "1"
- },
- {
- "domain": "float: -1.0 <= x <= 1.0",
- "help": "A patch reconstruction is accepted as a success and kept if its associated photometric consistency measure is above this threshold. Default: 0.7",
- "name": "pmvs-threshold",
- "type": "float",
- "value": "0.7"
- },
- {
- "domain": "positive integer",
- "help": "Each 3D point must be visible in at least minImageNum images for being reconstructed. 3 is suggested in general. Default: 3",
- "name": "pmvs-minImageNum",
- "type": "int",
- "value": "3"
- },
- {
- "domain": "float >= 1.0",
- "help": "Number of points per octree node, recommended and default value: 1",
- "name": "odm_meshing-samplesPerNode",
- "type": "float",
- "value": "1"
- },
- {
- "domain": "string",
- "help": "Skip filling of holes in the mesh. Default: false",
- "name": "mvs_texturing-skipHoleFilling",
- "type": "string",
- "value": "false"
- },
- {
- "domain": "string",
- "help": "Skip geometric visibility test. Default: false",
- "name": "mvs_texturing-skipGlobalSeamLeveling",
- "type": "string",
- "value": "false"
- },
- {
- "domain": "positive float",
- "help": "Override the ccd width information for the images",
- "name": "force-ccd",
- "type": "float",
- "value": "0"
- },
- {
- "domain": "",
- "help": "Generates a benchmark file with runtime info\nDefault: false",
- "name": "time",
- "type": "bool",
- "value": "false"
- },
- {
- "domain": "positive integer",
- "help": "The resolution of the output textures. Must be greater than textureWithSize. Default: 4096",
- "name": "odm_texturing-textureResolution",
- "type": "int",
- "value": "4096"
- },
- {
- "domain": "integer",
- "help": "Distance threshold in meters to find pre-matching images based on GPS exif data. Set to 0 to skip pre-matching. Default: 0",
- "name": "matcher-distance",
- "type": "int",
- "value": "0"
- },
- {
- "domain": "positive integer",
- "help": "Cell size controls the density of reconstructionsDefault: 2",
- "name": "pmvs-csize",
- "type": "int",
- "value": "2"
- },
- {
- "domain": "float > 0.0",
- "help": "Orthophoto ground resolution in pixels/meterDefault: 20",
- "name": "odm_orthophoto-resolution",
- "type": "float",
- "value": "20"
- },
- {
- "domain": "positive integer",
- "help": "Oct-tree depth at which the Laplacian equation is solved in the surface reconstruction step. Increasing this value increases computation times slightly but helps reduce memory usage. Default: 9",
- "name": "odm_meshing-solverDivide",
- "type": "int",
- "value": "9"
- },
- {
- "domain": "positive integer",
- "help": "pmvs samples wsize x wsize pixel colors from each image to compute photometric consistency score. For example, when wsize=7, 7x7=49 pixel colors are sampled in each image. Increasing the value leads to more stable reconstructions, but the program becomes slower. Default: 7",
- "name": "pmvs-wsize",
- "type": "int",
- "value": "7"
- },
- {
- "domain": "string",
- "help": "Keep faces in the mesh that are not seen in any camera. Default: false",
- "name": "mvs_texturing-keepUnseenFaces",
- "type": "string",
- "value": "false"
- },
- {
- "domain": "positive integer",
- "help": "The resolution to rescale the images performing the texturing. Default: 3600",
- "name": "odm_texturing-textureWithSize",
- "type": "int",
- "value": "3600"
- },
- {
- "domain": "string",
- "help": "Type of photometric outlier removal method: [none, gauss_damping, gauss_clamping]. Default: none",
- "name": "mvs_texturing-outlierRemovalType",
- "type": "string",
- "value": "none"
- },
- {
- "domain": "integer",
- "help": "Number of nearest images to pre-match based on GPS exif data. Set to 0 to skip pre-matching. Neighbors works together with Distance parameter, set both to 0 to not use pre-matching. OpenSFM uses both parameters at the same time, Bundler uses only one which has value, prefering the Neighbors parameter. Default: 8",
- "name": "matcher-neighbors",
- "type": "int",
- "value": "8"
- },
- {
- "domain": "string",
- "help": "Skip local seam blending. Default: false",
- "name": "mvs_texturing-skipLocalSeamLeveling",
- "type": "string",
- "value": "false"
- },
- {
- "domain": "string",
- "help": "Data term: [area, gmi]. Default: gmi",
- "name": "mvs_texturing-dataTerm",
- "type": "string",
- "value": "gmi"
- },
- {
- "domain": "string",
- "help": "Skip geometric visibility test. Default: false",
- "name": "mvs_texturing-skipGeometricVisibilityTest",
- "type": "string",
- "value": "false"
- },
- {
- "domain": "float",
- "help": "Ratio of the distance to the next best matched keypoint. Default: 0.6",
- "name": "matcher-ratio",
- "type": "float",
- "value": "0.6"
- }
-]
\ No newline at end of file
+{"--pmvs-num-cores": {"default": "1", "type": "", "metavar": "", "help": "The maximum number of cores to use in dense reconstruction. Default: %(default)s"}, "--cmvs-maxImages": {"default": "500", "type": "", "metavar": "", "help": "The maximum number of images per cluster. Default: %(default)s"}, "--odm_meshing-octreeDepth": {"default": "9", "type": "", "metavar": "", "help": "Oct-tree depth used in the mesh reconstruction, increase to get more vertices, recommended values are 8-12. Default: %(default)s"}, "--odm_meshing-maxVertexCount": {"default": "100000", "type": "", "metavar": "", "help": "The maximum vertex count of the output mesh Default: %(default)s"}, "--matcher-threshold": {"default": "2.0", "type": "", "metavar": "", "help": "Ignore matched keypoints if the two images share less than percent of keypoints. Default: %(default)s"}, "--min-num-features": {"default": "4000", "type": "", "metavar": "", "help": "Minimum number of features to extract per image. More features leads to better results but slower execution. Default: %(default)s"}, "--force-focal": {"type": "", "metavar": "", "help": "Override the focal length information for the images"}, "--resize-to": {"default": "2400", "type": "", "metavar": "", "help": "resizes images by the largest side"}, "--odm_georeferencing-useGcp": {"action": "store_true", "default": "False", "help": "Enabling GCPs from the file above. The GCP file is not used by default."}, "--pmvs-level": {"default": "1", "type": "", "metavar": "", "help": "The level in the image pyramid that is used for the computation. see http://www.di.ens.fr/pmvs/documentation.html for more pmvs documentation. Default: %(default)s"}, "--pmvs-threshold": {"default": "0.7", "type": "", "metavar": "", "help": "A patch reconstruction is accepted as a success and kept if its associated photometric consistency measure is above this threshold. Default: %(default)s"}, "--pmvs-minImageNum": {"default": "3", "type": "", "metavar": "", "help": "Each 3D point must be visible in at least minImageNum images for being reconstructed. 3 is suggested in general. Default: %(default)s"}, "--odm_meshing-samplesPerNode": {"default": "1.0", "type": "", "metavar": "= 1.0>", "help": "Number of points per octree node, recommended and default value: %(default)s"}, "--mvs_texturing-skipHoleFilling": {"default": "false", "metavar": "", "help": "Skip filling of holes in the mesh. Default: %(default)s"}, "--project-path": {"metavar": "", "help": "Path to the project to process"}, "--start-with": {"default": "resize", "choices": "['resize', 'opensfm', 'cmvs', 'pmvs', 'odm_meshing', 'mvs_texturing', 'odm_georeferencing', 'odm_orthophoto']", "help": "Can be one of: resize | opensfm | cmvs | pmvs | odm_meshing | mvs_texturing | odm_georeferencing | odm_orthophoto", "metavar": ""}, "--mvs_texturing-skipGlobalSeamLeveling": {"default": "false", "metavar": "", "help": "Skip geometric visibility test. Default: %(default)s"}, "--force-ccd": {"type": "", "metavar": "", "help": "Override the ccd width information for the images"}, "-h": {"action": "help", "default": "==SUPPRESS==", "help": "show this help message and exit"}, "--time": {"action": "store_true", "default": "False", "help": "Generates a benchmark file with runtime info\nDefault: %(default)s"}, "--odm_texturing-textureResolution": {"default": "4096", "type": "", "metavar": "", "help": "The resolution of the output textures. Must be greater than textureWithSize. Default: %(default)s"}, "--matcher-distance": {"default": "0", "type": "", "metavar": "", "help": "Distance threshold in meters to find pre-matching images based on GPS exif data. Set to 0 to skip pre-matching. Default: %(default)s"}, "--zip-results": {"action": "store_true", "default": "False", "help": "compress the results using gunzip"}, "--pmvs-csize": {"default": "2", "type": "", "metavar": "< positive integer>", "help": "Cell size controls the density of reconstructionsDefault: %(default)s"}, "--odm_orthophoto-resolution": {"default": "20.0", "type": "", "metavar": " 0.0>", "help": "Orthophoto ground resolution in pixels/meterDefault: %(default)s"}, "--odm_meshing-solverDivide": {"default": "9", "type": "", "metavar": "", "help": "Oct-tree depth at which the Laplacian equation is solved in the surface reconstruction step. Increasing this value increases computation times slightly but helps reduce memory usage. Default: %(default)s"}, "--odm_georeferencing-gcpFile": {"default": "gcp_list.txt", "metavar": "", "help": "path to the file containing the ground control points used for georeferencing. Default: %(default)s. The file needs to be on the following line format: \neasting northing height pixelrow pixelcol imagename"}, "--pmvs-wsize": {"default": "7", "type": "", "metavar": "", "help": "pmvs samples wsize x wsize pixel colors from each image to compute photometric consistency score. For example, when wsize=7, 7x7=49 pixel colors are sampled in each image. Increasing the value leads to more stable reconstructions, but the program becomes slower. Default: %(default)s"}, "--mvs_texturing-keepUnseenFaces": {"default": "false", "metavar": "", "help": "Keep faces in the mesh that are not seen in any camera. Default: %(default)s"}, "--odm_texturing-textureWithSize": {"default": "3600", "type": "", "metavar": "", "help": "The resolution to rescale the images performing the texturing. Default: %(default)s"}, "--mvs_texturing-outlierRemovalType": {"default": "none", "metavar": "", "help": "Type of photometric outlier removal method: [none, gauss_damping, gauss_clamping]. Default: %(default)s"}, "--matcher-neighbors": {"default": "8", "type": "", "metavar": "", "help": "Number of nearest images to pre-match based on GPS exif data. Set to 0 to skip pre-matching. Neighbors works together with Distance parameter, set both to 0 to not use pre-matching. OpenSFM uses both parameters at the same time, Bundler uses only one which has value, prefering the Neighbors parameter. Default: %(default)s"}, "--mvs_texturing-skipLocalSeamLeveling": {"default": "false", "metavar": "", "help": "Skip local seam blending. Default: %(default)s"}, "--end-with": {"default": "odm_orthophoto", "choices": "['resize', 'opensfm', 'cmvs', 'pmvs', 'odm_meshing', 'mvs_texturing', 'odm_georeferencing', 'odm_orthophoto']", "help": "Can be one of:resize | opensfm | cmvs | pmvs | odm_meshing | mvs_texturing | odm_georeferencing | odm_orthophoto", "metavar": ""}, "--mvs_texturing-dataTerm": {"default": "gmi", "metavar": "", "help": "Data term: [area, gmi]. Default: %(default)s"}, "--mvs_texturing-skipGeometricVisibilityTest": {"default": "false", "metavar": "", "help": "Skip geometric visibility test. Default: %(default)s"}, "--matcher-ratio": {"default": "0.6", "type": "", "metavar": "", "help": "Ratio of the distance to the next best matched keypoint. Default: %(default)s"}}
diff --git a/tests/odm_output.txt b/tests/odm_output.txt
new file mode 100644
index 0000000..de36114
--- /dev/null
+++ b/tests/odm_output.txt
@@ -0,0 +1,279 @@
+DJI_0131.JPG - DJI_0313.JPG has 1 candidate matches
+DJI_0131.JPG - DJI_0177.JPG has 3 candidate matches
+DJI_0131.JPG - DJI_0302.JPG has 0 candidate matches
+DJI_0131.JPG - DJI_0210.JPG has 0 candidate matches
+DJI_0131.JPG - DJI_0164.JPG has 1 candidate matches
+DJI_0131.JPG - DJI_0222.JPG has 0 candidate matches
+DJI_0131.JPG - DJI_0211.JPG has 1 candidate matches
+Matching DJI_0290.JPG - 205 / 252
+DJI_0290.JPG - DJI_0325.JPG has 1 candidate matches
+DJI_0290.JPG - DJI_0336.JPG has 0 candidate matches
+Matching DJI_0153.JPG - 206 / 252
+DJI_0153.JPG - DJI_0188.JPG has 1 candidate matches
+DJI_0153.JPG - DJI_0245.JPG has 3 candidate matches
+DJI_0153.JPG - DJI_0199.JPG has 0 candidate matches
+DJI_0153.JPG - DJI_0337.JPG has 0 candidate matches
+DJI_0153.JPG - DJI_0291.JPG has 2 candidate matches
+DJI_0153.JPG - DJI_0234.JPG has 0 candidate matches
+Matching DJI_0321.JPG - 207 / 252
+DJI_0321.JPG - DJI_0340.JPG has 2 candidate matches
+Matching DJI_0345.JPG - 208 / 252
+Matching DJI_0325.JPG - 209 / 252
+DJI_0325.JPG - DJI_0336.JPG has 5 candidate matches
+Matching DJI_0215.JPG - 210 / 252
+DJI_0215.JPG - DJI_0261.JPG has 0 candidate matches
+DJI_0215.JPG - DJI_0308.JPG has 1 candidate matches
+DJI_0215.JPG - DJI_0353.JPG has 1 candidate matches
+DJI_0215.JPG - DJI_0218.JPG has 3 candidate matches
+Matching DJI_0284.JPG - 211 / 252
+DJI_0284.JPG - DJI_0329.JPG has 1 candidate matches
+DJI_0284.JPG - DJI_0286.JPG has 0 candidate matches
+DJI_0284.JPG - DJI_0332.JPG has 2 candidate matches
+Matching DJI_0156.JPG - 212 / 252
+DJI_0156.JPG - DJI_0294.JPG has 1 candidate matches
+DJI_0156.JPG - DJI_0231.JPG has 2 candidate matches
+DJI_0156.JPG - DJI_0248.JPG has 0 candidate matches
+DJI_0156.JPG - DJI_0185.JPG has 13 candidate matches
+DJI_0156.JPG - DJI_0276.JPG has 0 candidate matches
+DJI_0156.JPG - DJI_0202.JPG has 3 candidate matches
+Matching DJI_0108.JPG - 213 / 252
+DJI_0108.JPG - DJI_0188.JPG has 0 candidate matches
+DJI_0108.JPG - DJI_0279.JPG has 0 candidate matches
+DJI_0108.JPG - DJI_0153.JPG has 0 candidate matches
+DJI_0108.JPG - DJI_0200.JPG has 0 candidate matches
+DJI_0108.JPG - DJI_0199.JPG has 0 candidate matches
+DJI_0108.JPG - DJI_0234.JPG has 0 candidate matches
+DJI_0108.JPG - DJI_0291.JPG has 0 candidate matches
+DJI_0108.JPG - DJI_0142.JPG has 5 candidate matches
+Matching DJI_0174.JPG - 214 / 252
+DJI_0174.JPG - DJI_0213.JPG has 0 candidate matches
+DJI_0174.JPG - DJI_0220.JPG has 0 candidate matches
+DJI_0174.JPG - DJI_0259.JPG has 0 candidate matches
+DJI_0174.JPG - DJI_0266.JPG has 1 candidate matches
+DJI_0174.JPG - DJI_0305.JPG has 0 candidate matches
+Matching DJI_0324.JPG - 215 / 252
+DJI_0324.JPG - DJI_0337.JPG has 1 candidate matches
+Matching DJI_0116.JPG - 216 / 252
+DJI_0116.JPG - DJI_0134.JPG has 0 candidate matches
+DJI_0116.JPG - DJI_0299.JPG has 0 candidate matches
+DJI_0116.JPG - DJI_0271.JPG has 0 candidate matches
+DJI_0116.JPG - DJI_0161.JPG has 1 candidate matches
+DJI_0116.JPG - DJI_0208.JPG has 0 candidate matches
+DJI_0116.JPG - DJI_0225.JPG has 0 candidate matches
+DJI_0116.JPG - DJI_0345.JPG has 0 candidate matches
+DJI_0116.JPG - DJI_0254.JPG has 1 candidate matches
+DJI_0116.JPG - DJI_0179.JPG has 1 candidate matches
+Matching DJI_0247.JPG - 217 / 252
+DJI_0247.JPG - DJI_0278.JPG has 1 candidate matches
+DJI_0247.JPG - DJI_0323.JPG has 0 candidate matches
+DJI_0247.JPG - DJI_0339.JPG has 0 candidate matches
+DJI_0247.JPG - DJI_0338.JPG has 0 candidate matches
+Matching DJI_0220.JPG - 218 / 252
+DJI_0220.JPG - DJI_0266.JPG has 0 candidate matches
+DJI_0220.JPG - DJI_0351.JPG has 1 candidate matches
+DJI_0220.JPG - DJI_0305.JPG has 0 candidate matches
+DJI_0220.JPG - DJI_0310.JPG has 0 candidate matches
+Matching DJI_0128.JPG - 219 / 252
+DJI_0128.JPG - DJI_0174.JPG has 1 candidate matches
+DJI_0128.JPG - DJI_0213.JPG has 0 candidate matches
+DJI_0128.JPG - DJI_0310.JPG has 0 candidate matches
+DJI_0128.JPG - DJI_0167.JPG has 1 candidate matches
+DJI_0128.JPG - DJI_0351.JPG has 0 candidate matches
+DJI_0128.JPG - DJI_0305.JPG has 0 candidate matches
+DJI_0128.JPG - DJI_0260.JPG has 0 candidate matches
+DJI_0128.JPG - DJI_0220.JPG has 0 candidate matches
+Matching DJI_0183.JPG - 220 / 252
+DJI_0183.JPG - DJI_0250.JPG has 0 candidate matches
+DJI_0183.JPG - DJI_0274.JPG has 1 candidate matches
+DJI_0183.JPG - DJI_0229.JPG has 0 candidate matches
+DJI_0183.JPG - DJI_0204.JPG has 7 candidate matches
+DJI_0183.JPG - DJI_0319.JPG has 2 candidate matches
+DJI_0183.JPG - DJI_0341.JPG has 0 candidate matches
+DJI_0183.JPG - DJI_0296.JPG has 0 candidate matches
+Matching DJI_0252.JPG - 221 / 252
+DJI_0252.JPG - DJI_0317.JPG has 0 candidate matches
+DJI_0252.JPG - DJI_0343.JPG has 1 candidate matches
+DJI_0252.JPG - DJI_0318.JPG has 0 candidate matches
+DJI_0252.JPG - DJI_0298.JPG has 0 candidate matches
+DJI_0252.JPG - DJI_0273.JPG has 7 candidate matches
+Matching DJI_0308.JPG - 222 / 252
+DJI_0308.JPG - DJI_0353.JPG has 6 candidate matches
+Matching DJI_0194.JPG - 223 / 252
+DJI_0194.JPG - DJI_0239.JPG has 7 candidate matches
+DJI_0194.JPG - DJI_0285.JPG has 0 candidate matches
+DJI_0194.JPG - DJI_0286.JPG has 0 candidate matches
+DJI_0194.JPG - DJI_0331.JPG has 2 candidate matches
+DJI_0194.JPG - DJI_0240.JPG has 1 candidate matches
+DJI_0194.JPG - DJI_0329.JPG has 0 candidate matches
+DJI_0194.JPG - DJI_0330.JPG has 0 candidate matches
+Matching DJI_0175.JPG - 224 / 252
+DJI_0175.JPG - DJI_0212.JPG has 2 candidate matches
+DJI_0175.JPG - DJI_0221.JPG has 1 candidate matches
+DJI_0175.JPG - DJI_0267.JPG has 1 candidate matches
+DJI_0175.JPG - DJI_0349.JPG has 1 candidate matches
+DJI_0175.JPG - DJI_0304.JPG has 0 candidate matches
+Matching DJI_0246.JPG - 225 / 252
+DJI_0246.JPG - DJI_0292.JPG has 1 candidate matches
+DJI_0246.JPG - DJI_0324.JPG has 0 candidate matches
+DJI_0246.JPG - DJI_0279.JPG has 1 candidate matches
+Matching DJI_0208.JPG - 226 / 252
+DJI_0208.JPG - DJI_0271.JPG has 0 candidate matches
+DJI_0208.JPG - DJI_0225.JPG has 0 candidate matches
+DJI_0208.JPG - DJI_0254.JPG has 1 candidate matches
+DJI_0208.JPG - DJI_0345.JPG has 2 candidate matches
+DJI_0208.JPG - DJI_0316.JPG has 0 candidate matches
+Matching DJI_0225.JPG - 227 / 252
+DJI_0225.JPG - DJI_0345.JPG has 0 candidate matches
+DJI_0225.JPG - DJI_0299.JPG has 1 candidate matches
+DJI_0225.JPG - DJI_0316.JPG has 0 candidate matches
+DJI_0225.JPG - DJI_0254.JPG has 1 candidate matches
+DJI_0225.JPG - DJI_0271.JPG has 0 candidate matches
+Matching DJI_0210.JPG - 228 / 252
+DJI_0210.JPG - DJI_0347.JPG has 0 candidate matches
+DJI_0210.JPG - DJI_0223.JPG has 1 candidate matches
+DJI_0210.JPG - DJI_0256.JPG has 0 candidate matches
+DJI_0210.JPG - DJI_0269.JPG has 0 candidate matches
+Matching DJI_0185.JPG - 229 / 252
+DJI_0185.JPG - DJI_0248.JPG has 0 candidate matches
+DJI_0185.JPG - DJI_0231.JPG has 1 candidate matches
+DJI_0185.JPG - DJI_0276.JPG has 1 candidate matches
+DJI_0185.JPG - DJI_0294.JPG has 1 candidate matches
+DJI_0185.JPG - DJI_0321.JPG has 0 candidate matches
+DJI_0185.JPG - DJI_0202.JPG has 26 candidate matches
+Robust matching time : 0.00102090835571s
+Full matching 23 / 26, time: 0.113751173019s
+Matching DJI_0333.JPG - 230 / 252
+Matching DJI_0137.JPG - 231 / 252
+DJI_0137.JPG - DJI_0250.JPG has 0 candidate matches
+DJI_0137.JPG - DJI_0158.JPG has 16 candidate matches
+DJI_0137.JPG - DJI_0183.JPG has 3 candidate matches
+DJI_0137.JPG - DJI_0296.JPG has 0 candidate matches
+DJI_0137.JPG - DJI_0204.JPG has 2 candidate matches
+DJI_0137.JPG - DJI_0319.JPG has 0 candidate matches
+DJI_0137.JPG - DJI_0229.JPG has 0 candidate matches
+DJI_0137.JPG - DJI_0274.JPG has 0 candidate matches
+Matching DJI_0150.JPG - 232 / 252
+DJI_0150.JPG - DJI_0191.JPG has 1 candidate matches
+DJI_0150.JPG - DJI_0288.JPG has 1 candidate matches
+DJI_0150.JPG - DJI_0334.JPG has 0 candidate matches
+DJI_0150.JPG - DJI_0237.JPG has 0 candidate matches
+DJI_0150.JPG - DJI_0196.JPG has 0 candidate matches
+DJI_0150.JPG - DJI_0242.JPG has 1 candidate matches
+Matching DJI_0249.JPG - 233 / 252
+DJI_0249.JPG - DJI_0321.JPG has 1 candidate matches
+DJI_0249.JPG - DJI_0340.JPG has 1 candidate matches
+DJI_0249.JPG - DJI_0276.JPG has 3 candidate matches
+Matching DJI_0283.JPG - 234 / 252
+DJI_0283.JPG - DJI_0328.JPG has 0 candidate matches
+DJI_0283.JPG - DJI_0333.JPG has 0 candidate matches
+DJI_0283.JPG - DJI_0287.JPG has 1 candidate matches
+Matching DJI_0256.JPG - 235 / 252
+DJI_0256.JPG - DJI_0301.JPG has 0 candidate matches
+DJI_0256.JPG - DJI_0346.JPG has 2 candidate matches
+DJI_0256.JPG - DJI_0347.JPG has 0 candidate matches
+DJI_0256.JPG - DJI_0314.JPG has 0 candidate matches
+DJI_0256.JPG - DJI_0269.JPG has 4 candidate matches
+Matching DJI_0235.JPG - 236 / 252
+DJI_0235.JPG - DJI_0336.JPG has 1 candidate matches
+DJI_0235.JPG - DJI_0326.JPG has 1 candidate matches
+DJI_0235.JPG - DJI_0281.JPG has 0 candidate matches
+DJI_0235.JPG - DJI_0290.JPG has 0 candidate matches
+DJI_0235.JPG - DJI_0244.JPG has 2 candidate matches
+Matching DJI_0277.JPG - 237 / 252
+DJI_0277.JPG - DJI_0322.JPG has 0 candidate matches
+DJI_0277.JPG - DJI_0339.JPG has 0 candidate matches
+DJI_0277.JPG - DJI_0293.JPG has 3 candidate matches
+Matching DJI_0296.JPG - 238 / 252
+DJI_0296.JPG - DJI_0319.JPG has 1 candidate matches
+DJI_0296.JPG - DJI_0342.JPG has 0 candidate matches
+Matching DJI_0157.JPG - 239 / 252
+DJI_0157.JPG - DJI_0295.JPG has 0 candidate matches
+DJI_0157.JPG - DJI_0340.JPG has 0 candidate matches
+DJI_0157.JPG - DJI_0184.JPG has 12 candidate matches
+DJI_0157.JPG - DJI_0230.JPG has 0 candidate matches
+DJI_0157.JPG - DJI_0203.JPG has 3 candidate matches
+DJI_0157.JPG - DJI_0249.JPG has 0 candidate matches
+DJI_0157.JPG - DJI_0275.JPG has 0 candidate matches
+Matching DJI_0273.JPG - 240 / 252
+DJI_0273.JPG - DJI_0318.JPG has 1 candidate matches
+DJI_0273.JPG - DJI_0343.JPG has 2 candidate matches
+DJI_0273.JPG - DJI_0298.JPG has 0 candidate matches
+Matching DJI_0148.JPG - 241 / 252
+DJI_0148.JPG - DJI_0331.JPG has 0 candidate matches
+DJI_0148.JPG - DJI_0193.JPG has 7 candidate matches
+DJI_0148.JPG - DJI_0285.JPG has 0 candidate matches
+DJI_0148.JPG - DJI_0194.JPG has 1 candidate matches
+DJI_0148.JPG - DJI_0330.JPG has 1 candidate matches
+DJI_0148.JPG - DJI_0286.JPG has 0 candidate matches
+DJI_0148.JPG - DJI_0240.JPG has 3 candidate matches
+DJI_0148.JPG - DJI_0239.JPG has 0 candidate matches
+DJI_0148.JPG - DJI_0329.JPG has 3 candidate matches
+DJI_0148.JPG - DJI_0332.JPG has 1 candidate matches
+Matching DJI_0162.JPG - 242 / 252
+DJI_0162.JPG - DJI_0179.JPG has 16 candidate matches
+DJI_0162.JPG - DJI_0255.JPG has 1 candidate matches
+DJI_0162.JPG - DJI_0208.JPG has 3 candidate matches
+DJI_0162.JPG - DJI_0315.JPG has 0 candidate matches
+DJI_0162.JPG - DJI_0224.JPG has 0 candidate matches
+DJI_0162.JPG - DJI_0254.JPG has 2 candidate matches
+DJI_0162.JPG - DJI_0300.JPG has 2 candidate matches
+Matching DJI_0236.JPG - 243 / 252
+DJI_0236.JPG - DJI_0289.JPG has 2 candidate matches
+DJI_0236.JPG - DJI_0243.JPG has 1 candidate matches
+DJI_0236.JPG - DJI_0282.JPG has 2 candidate matches
+DJI_0236.JPG - DJI_0327.JPG has 1 candidate matches
+DJI_0236.JPG - DJI_0335.JPG has 0 candidate matches
+Matching DJI_0298.JPG - 244 / 252
+DJI_0298.JPG - DJI_0343.JPG has 0 candidate matches
+DJI_0298.JPG - DJI_0344.JPG has 0 candidate matches
+DJI_0298.JPG - DJI_0317.JPG has 2 candidate matches
+Matching DJI_0228.JPG - 245 / 252
+DJI_0228.JPG - DJI_0251.JPG has 6 candidate matches
+DJI_0228.JPG - DJI_0274.JPG has 0 candidate matches
+DJI_0228.JPG - DJI_0342.JPG has 0 candidate matches
+DJI_0228.JPG - DJI_0319.JPG has 0 candidate matches
+Matching DJI_0322.JPG - 246 / 252
+DJI_0322.JPG - DJI_0339.JPG has 3 candidate matches
+Matching DJI_0176.JPG - 247 / 252
+DJI_0176.JPG - DJI_0222.JPG has 1 candidate matches
+DJI_0176.JPG - DJI_0211.JPG has 2 candidate matches
+DJI_0176.JPG - DJI_0312.JPG has 1 candidate matches
+DJI_0176.JPG - DJI_0257.JPG has 0 candidate matches
+DJI_0176.JPG - DJI_0258.JPG has 1 candidate matches
+DJI_0176.JPG - DJI_0268.JPG has 0 candidate matches
+DJI_0176.JPG - DJI_0303.JPG has 0 candidate matches
+Matching DJI_0272.JPG - 248 / 252
+DJI_0272.JPG - DJI_0344.JPG has 0 candidate matches
+DJI_0272.JPG - DJI_0317.JPG has 0 candidate matches
+DJI_0272.JPG - DJI_0298.JPG has 3 candidate matches
+DJI_0272.JPG - DJI_0299.JPG has 3 candidate matches
+Matching DJI_0124.JPG - 249 / 252
+DJI_0124.JPG - DJI_0307.JPG has 0 candidate matches
+DJI_0124.JPG - DJI_0263.JPG has 1 candidate matches
+DJI_0124.JPG - DJI_0215.JPG has 0 candidate matches
+DJI_0124.JPG - DJI_0169.JPG has 1 candidate matches
+DJI_0124.JPG - DJI_0126.JPG has 1 candidate matches
+DJI_0124.JPG - DJI_0170.JPG has 0 candidate matches
+DJI_0124.JPG - DJI_0261.JPG has 1 candidate matches
+DJI_0124.JPG - DJI_0125.JPG has 2 candidate matches
+DJI_0124.JPG - DJI_0172.JPG has 1 candidate matches
+DJI_0124.JPG - DJI_0216.JPG has 0 candidate matches
+DJI_0124.JPG - DJI_0171.JPG has 0 candidate matches
+DJI_0124.JPG - DJI_0218.JPG has 1 candidate matches
+Matching DJI_0310.JPG - 250 / 252
+DJI_0310.JPG - DJI_0351.JPG has 1 candidate matches
+Matching DJI_0241.JPG - 251 / 252
+DJI_0241.JPG - DJI_0333.JPG has 1 candidate matches
+DJI_0241.JPG - DJI_0328.JPG has 0 candidate matches
+DJI_0241.JPG - DJI_0284.JPG has 1 candidate matches
+DJI_0241.JPG - DJI_0332.JPG has 2 candidate matches
+DJI_0241.JPG - DJI_0287.JPG has 1 candidate matches
+Matching DJI_0118.JPG - 252 / 252
+DJI_0118.JPG - DJI_0178.JPG has 0 candidate matches
+DJI_0118.JPG - DJI_0301.JPG has 0 candidate matches
+DJI_0118.JPG - DJI_0269.JPG has 1 candidate matches
+DJI_0118.JPG - DJI_0256.JPG has 1 candidate matches
+DJI_0118.JPG - DJI_0210.JPG has 0 candidate matches
+DJI_0118.JPG - DJI_0132.JPG has 3 candidate matches
+DJI_0118.JPG - DJI_0223.JPG has 1 candidate matches
+DJI_0118.JPG - DJI_0163.JPG has 1 candidate matches
\ No newline at end of file