Test mode, bug fixing, automatic linting

pull/1/head
Piero Toffanin 2016-09-25 18:35:44 -04:00
rodzic 3230c4b45a
commit 36ffae3389
14 zmienionych plików z 391 dodań i 245 usunięć

16
Gruntfile.js 100644
Wyświetl plik

@ -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']);
};

Wyświetl plik

@ -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).

Wyświetl plik

@ -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 => {

Wyświetl plik

@ -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 <http://www.gnu.org/licenses/>.
*/
"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;

Wyświetl plik

@ -17,6 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"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){

Wyświetl plik

@ -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]){

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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{

Wyświetl plik

@ -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 = [];

Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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){

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -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