GeoTagController.cc 10.9 KB
Newer Older
Don Gagne's avatar
Don Gagne committed
1 2 3 4 5 6 7 8 9 10
/****************************************************************************
 *
 *   (c) 2009-2016 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
 *
 * QGroundControl is licensed according to the terms in the file
 * COPYING.md in the root of the source code directory.
 *
 ****************************************************************************/

#include "GeoTagController.h"
11
#include "QGCQFileDialog.h"
12
#include "QGCLoggingCategory.h"
13
#include "MainWindow.h"
14
#include <math.h>
Andreas Bircher's avatar
Andreas Bircher committed
15
#include <QtEndian>
16 17
#include <QMessageBox>
#include <QDebug>
Andreas Bircher's avatar
Andreas Bircher committed
18
#include <cfloat>
Don Gagne's avatar
Don Gagne committed
19

20 21 22 23
#include "ExifParser.h"
#include "ULogParser.h"
#include "PX4LogParser.h"

Don Gagne's avatar
Don Gagne committed
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
GeoTagController::GeoTagController(void)
    : _progress(0)
    , _inProgress(false)
{
    connect(&_worker, &GeoTagWorker::progressChanged,   this, &GeoTagController::_workerProgressChanged);
    connect(&_worker, &GeoTagWorker::error,             this, &GeoTagController::_workerError);
    connect(&_worker, &GeoTagWorker::started,           this, &GeoTagController::inProgressChanged);
    connect(&_worker, &GeoTagWorker::finished,          this, &GeoTagController::inProgressChanged);
}

GeoTagController::~GeoTagController()
{

}

void GeoTagController::pickLogFile(void)
{
Gus Grubba's avatar
Gus Grubba committed
41
    QString filename = QGCQFileDialog::getOpenFileName(MainWindow::instance(), tr("Select log file load"), QString(), tr("ULog file (*.ulg);;PX4 log file (*.px4log);;All Files (*.*)"));
Don Gagne's avatar
Don Gagne committed
42 43 44 45 46 47 48 49
    if (!filename.isEmpty()) {
        _worker.setLogFile(filename);
        emit logFileChanged(filename);
    }
}

void GeoTagController::pickImageDirectory(void)
{
Gus Grubba's avatar
Gus Grubba committed
50
    QString dir = QGCQFileDialog::getExistingDirectory(MainWindow::instance(), tr("Select image directory"));
Don Gagne's avatar
Don Gagne committed
51 52 53 54 55 56
    if (!dir.isEmpty()) {
        _worker.setImageDirectory(dir);
        emit imageDirectoryChanged(dir);
    }
}

57 58
void GeoTagController::pickSaveDirectory(void)
{
Gus Grubba's avatar
Gus Grubba committed
59
    QString dir = QGCQFileDialog::getExistingDirectory(MainWindow::instance(), tr("Select save directory"));
60 61 62 63 64 65
    if (!dir.isEmpty()) {
        _worker.setSaveDirectory(dir);
        emit saveDirectoryChanged(dir);
    }
}

Don Gagne's avatar
Don Gagne committed
66 67 68 69
void GeoTagController::startTagging(void)
{
    _errorMessage.clear();
    emit errorMessageChanged(_errorMessage);
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131

    QDir imageDirectory = QDir(_worker.imageDirectory());
    if(!imageDirectory.exists()) {
        _errorMessage = tr("Cannot find the image directory");
        emit errorMessageChanged(_errorMessage);
        return;
    }
    if(_worker.saveDirectory() == "") {
        if(!imageDirectory.mkdir(_worker.imageDirectory() + "/TAGGED")) {
            QMessageBox msgBox(QMessageBox::Question,
                               tr("Images have alreay been tagged."),
                               tr("The images have already been tagged. Do you want to replace the previously tagged images?"),
                               QMessageBox::Cancel);
            msgBox.setWindowModality(Qt::ApplicationModal);
            msgBox.addButton(tr("Replace"), QMessageBox::ActionRole);
            if (msgBox.exec() == QMessageBox::Cancel) {
                _errorMessage = tr("Images have already been tagged");
                emit errorMessageChanged(_errorMessage);
                return;
            }
            QDir oldTaggedFolder = QDir(_worker.imageDirectory() + "/TAGGED");
            oldTaggedFolder.removeRecursively();
            if(!imageDirectory.mkdir(_worker.imageDirectory() + "/TAGGED")) {
                _errorMessage = tr("Couldn't replace the previously tagged images");
                emit errorMessageChanged(_errorMessage);
                return;
            }
        }
    } else {
        QDir saveDirectory = QDir(_worker.saveDirectory());
        if(!saveDirectory.exists()) {
            _errorMessage = tr("Cannot find the save directory");
            emit errorMessageChanged(_errorMessage);
            return;
        }
        saveDirectory.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks | QDir::Writable);
        QStringList nameFilters;
        nameFilters << "*.jpg" << "*.JPG";
        saveDirectory.setNameFilters(nameFilters);
        QStringList imageList = saveDirectory.entryList();
        if(!imageList.isEmpty()) {
            QMessageBox msgBox(QMessageBox::Question,
                               tr("Save folder not empty."),
                               tr("The save folder already contains images. Do you want to replace them?"),
                               QMessageBox::Cancel);
            msgBox.setWindowModality(Qt::ApplicationModal);
            msgBox.addButton(tr("Replace"), QMessageBox::ActionRole);
            if (msgBox.exec() == QMessageBox::Cancel) {
                _errorMessage = tr("Save folder not empty");
                emit errorMessageChanged(_errorMessage);
                return;
            }
            foreach(QString dirFile, imageList)
            {
                if(!saveDirectory.remove(dirFile)) {
                    _errorMessage = tr("Couldn't replace the existing images");
                    emit errorMessageChanged(_errorMessage);
                    return;
                }
            }
        }
    }
Don Gagne's avatar
Don Gagne committed
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
    _worker.start();
}

void GeoTagController::_workerProgressChanged(double progress)
{
    _progress = progress;
    emit progressChanged(progress);
}

void GeoTagController::_workerError(QString errorMessage)
{
    _errorMessage = errorMessage;
    emit errorMessageChanged(errorMessage);
}

GeoTagWorker::GeoTagWorker(void)
    : _cancel(false)
149 150 151
    , _logFile("")
    , _imageDirectory("")
    , _saveDirectory("")
Don Gagne's avatar
Don Gagne committed
152 153 154 155 156 157 158
{

}

void GeoTagWorker::run(void)
{
    _cancel = false;
159 160
    emit progressChanged(1);
    double nSteps = 5;
Andreas Bircher's avatar
Andreas Bircher committed
161

162 163
    // Load Images
    _imageList.clear();
Andreas Bircher's avatar
Andreas Bircher committed
164 165 166 167 168 169
    QDir imageDirectory = QDir(_imageDirectory);
    imageDirectory.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks | QDir::Writable);
    imageDirectory.setSorting(QDir::Name);
    QStringList nameFilters;
    nameFilters << "*.jpg" << "*.JPG";
    imageDirectory.setNameFilters(nameFilters);
170 171
    _imageList = imageDirectory.entryInfoList();
    if(_imageList.isEmpty()) {
Andreas Bircher's avatar
Andreas Bircher committed
172 173 174
        emit error(tr("The image directory doesn't contain images, make sure your images are of the JPG format"));
        return;
    }
175
    emit progressChanged((100/nSteps));
Andreas Bircher's avatar
Andreas Bircher committed
176

177 178
    // Parse EXIF
    ExifParser exifParser;
179
    _imageTime.clear();
180 181
    for (int i = 0; i < _imageList.size(); ++i) {
        QFile file(_imageList.at(i).absoluteFilePath());
Andreas Bircher's avatar
Andreas Bircher committed
182
        if (!file.open(QIODevice::ReadOnly)) {
183 184
            emit error(tr("Geotagging failed. Couldn't open an image."));
            return;
Andreas Bircher's avatar
Andreas Bircher committed
185
        }
186
        QByteArray imageBuffer = file.readAll();
Andreas Bircher's avatar
Andreas Bircher committed
187 188
        file.close();

189
        _imageTime.append(exifParser.readTime(imageBuffer));
Andreas Bircher's avatar
Andreas Bircher committed
190

191
        emit progressChanged((100/nSteps) + ((100/nSteps) / _imageList.size())*i);
Andreas Bircher's avatar
Andreas Bircher committed
192

193 194 195 196 197
        if (_cancel) {
            qCDebug(GeotaggingLog) << "Tagging cancelled";
            emit error(tr("Tagging cancelled"));
            return;
        }
Andreas Bircher's avatar
Andreas Bircher committed
198 199
    }

200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
    // Load log
    bool isULog = _logFile.endsWith(".ulg", Qt::CaseSensitive);
    QFile file(_logFile);
    if (!file.open(QIODevice::ReadOnly)) {
        emit error(tr("Geotagging failed. Couldn't open log file."));
        return;
    }
    QByteArray log = file.readAll();
    file.close();

    // Instantiate appropriate parser
    _triggerList.clear();
    bool parseComplete = false;
    if(isULog) {
        ULogParser parser;
        parseComplete = parser.getTagsFromLog(log, _triggerList);

    } else {
        PX4LogParser parser;
        parseComplete = parser.getTagsFromLog(log, _triggerList);

    }

    if (!parseComplete) {
224 225 226 227 228 229 230 231 232
        if (_cancel) {
            qCDebug(GeotaggingLog) << "Tagging cancelled";
            emit error(tr("Tagging cancelled"));
            return;
        } else {
            qCDebug(GeotaggingLog) << "Log parsing failed";
            emit error(tr("Log parsing failed - tagging cancelled"));
            return;
        }
Andreas Bircher's avatar
Andreas Bircher committed
233
    }
234
    emit progressChanged(3*(100/nSteps));
Andreas Bircher's avatar
Andreas Bircher committed
235

236
    qCDebug(GeotaggingLog) << "Found " << _triggerList.count() << " trigger logs.";
Andreas Bircher's avatar
Andreas Bircher committed
237

238 239 240 241 242
    if (_cancel) {
        qCDebug(GeotaggingLog) << "Tagging cancelled";
        emit error(tr("Tagging cancelled"));
        return;
    }
Andreas Bircher's avatar
Andreas Bircher committed
243

244
    // Filter Trigger
Andreas Bircher's avatar
Andreas Bircher committed
245
    if (!triggerFiltering()) {
246 247
        qCDebug(GeotaggingLog) << "Geotagging failed in trigger filtering";
        emit error(tr("Geotagging failed in trigger filtering"));
Andreas Bircher's avatar
Andreas Bircher committed
248 249
        return;
    }
250
    emit progressChanged(4*(100/nSteps));
Andreas Bircher's avatar
Andreas Bircher committed
251

252 253 254 255 256
    if (_cancel) {
        qCDebug(GeotaggingLog) << "Tagging cancelled";
        emit error(tr("Tagging cancelled"));
        return;
    }
Andreas Bircher's avatar
Andreas Bircher committed
257

258 259 260 261
    // Tag images
    int maxIndex = std::min(_imageIndices.count(), _triggerIndices.count());
    maxIndex = std::min(maxIndex, _imageList.count());
    for(int i = 0; i < maxIndex; i++) {
262 263 264 265 266
        int imageIndex = _imageIndices[i];
        if (imageIndex >= _imageList.count()) {
            emit error(tr("Geotagging failed. Image requested not present."));
            return;
        }
267 268 269 270 271 272 273
        QFile fileRead(_imageList.at(_imageIndices[i]).absoluteFilePath());
        if (!fileRead.open(QIODevice::ReadOnly)) {
            emit error(tr("Geotagging failed. Couldn't open an image."));
            return;
        }
        QByteArray imageBuffer = fileRead.readAll();
        fileRead.close();
Andreas Bircher's avatar
Andreas Bircher committed
274

275
        if (!exifParser.write(imageBuffer, _triggerList[_triggerIndices[i]])) {
276 277
            emit error(tr("Geotagging failed. Couldn't write to image."));
            return;
Andreas Bircher's avatar
Andreas Bircher committed
278
        } else {
279 280 281 282 283 284 285 286 287
            QFile fileWrite;
            if(_saveDirectory == "") {
                fileWrite.setFileName(_imageDirectory + "/TAGGED/" + _imageList.at(_imageIndices[i]).fileName());
            } else {
                fileWrite.setFileName(_saveDirectory + "/" + _imageList.at(_imageIndices[i]).fileName());
            }
            if (!fileWrite.open(QFile::WriteOnly)) {
                emit error(tr("Geotagging failed. Couldn't write to an image."));
                return;
Andreas Bircher's avatar
Andreas Bircher committed
288
            }
289 290
            fileWrite.write(imageBuffer);
            fileWrite.close();
Andreas Bircher's avatar
Andreas Bircher committed
291
        }
292
        emit progressChanged(4*(100/nSteps) + ((100/nSteps) / maxIndex)*i);
Andreas Bircher's avatar
Andreas Bircher committed
293

Don Gagne's avatar
Don Gagne committed
294
        if (_cancel) {
295
            qCDebug(GeotaggingLog) << "Tagging cancelled";
Don Gagne's avatar
Don Gagne committed
296 297 298 299 300
            emit error(tr("Tagging cancelled"));
            return;
        }
    }

301 302 303 304 305 306
    if (_cancel) {
        qCDebug(GeotaggingLog) << "Tagging cancelled";
        emit error(tr("Tagging cancelled"));
        return;
    }

Don Gagne's avatar
Don Gagne committed
307 308
    emit progressChanged(100);
}
Andreas Bircher's avatar
Andreas Bircher committed
309 310 311

bool GeoTagWorker::triggerFiltering()
{
312 313
    _imageIndices.clear();
    _triggerIndices.clear();
314 315 316 317 318 319 320 321 322

    if(_imageList.count() > _triggerList.count()) {             // Logging dropouts
        qCDebug(GeotaggingLog) << "Detected missing feedback packets.";
    } else if (_imageList.count() < _triggerList.count()) {     // Camera skipped frames
        qCDebug(GeotaggingLog) << "Detected missing image frames.";
    }

    for(int i = 0; i < _imageList.count() && i < _triggerList.count(); i++) {
        _imageIndices.append(_triggerList[i].imageSequence);
Andreas Bircher's avatar
Andreas Bircher committed
323 324
        _triggerIndices.append(i);
    }
325

Andreas Bircher's avatar
Andreas Bircher committed
326 327
    return true;
}