GeoTagController.cc 11 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"
Gus Grubba's avatar
Gus Grubba committed
11
//#include "QGCQFileDialog.h"
12 13
#include "QGCLoggingCategory.h"
#include <math.h>
Andreas Bircher's avatar
Andreas Bircher committed
14
#include <QtEndian>
15 16
#include <QMessageBox>
#include <QDebug>
Andreas Bircher's avatar
Andreas Bircher committed
17
#include <cfloat>
Gus Grubba's avatar
Gus Grubba committed
18
#include <QDir>
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 = QString(); //--TODO: 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 = QString(); //--TODO: 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 = QString(); //--TODO: 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

    QDir imageDirectory = QDir(_worker.imageDirectory());
    if(!imageDirectory.exists()) {
73
        _setErrorMessage(tr("Cannot find the image directory"));
74 75 76 77
        return;
    }
    if(_worker.saveDirectory() == "") {
        if(!imageDirectory.mkdir(_worker.imageDirectory() + "/TAGGED")) {
Gus Grubba's avatar
Gus Grubba committed
78 79
            //--TODO:
            /*
80 81 82 83 84 85 86
            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) {
87
                _setErrorMessage(tr("Images have already been tagged"));
88 89
                return;
            }
Gus Grubba's avatar
Gus Grubba committed
90
            */
91 92 93
            QDir oldTaggedFolder = QDir(_worker.imageDirectory() + "/TAGGED");
            oldTaggedFolder.removeRecursively();
            if(!imageDirectory.mkdir(_worker.imageDirectory() + "/TAGGED")) {
94
                _setErrorMessage(tr("Couldn't replace the previously tagged images"));
95 96 97 98 99 100
                return;
            }
        }
    } else {
        QDir saveDirectory = QDir(_worker.saveDirectory());
        if(!saveDirectory.exists()) {
101
            _setErrorMessage(tr("Cannot find the save directory"));
102 103 104 105 106 107 108 109
            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()) {
Gus Grubba's avatar
Gus Grubba committed
110 111
            //--TODO:
            /*
112 113 114 115 116 117 118
            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) {
119
                _setErrorMessage(tr("Save folder not empty"));
120 121
                return;
            }
Gus Grubba's avatar
Gus Grubba committed
122
            */
123
            for(QString dirFile: imageList)
124 125
            {
                if(!saveDirectory.remove(dirFile)) {
126
                    _setErrorMessage(tr("Couldn't replace the existing images"));
127 128 129 130 131
                    return;
                }
            }
        }
    }
Don Gagne's avatar
Don Gagne committed
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
    _worker.start();
}

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

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

147 148 149 150 151 152 153

void GeoTagController::_setErrorMessage(const QString& error)
{
    _errorMessage = error;
    emit errorMessageChanged(error);
}

Don Gagne's avatar
Don Gagne committed
154 155
GeoTagWorker::GeoTagWorker(void)
    : _cancel(false)
156 157 158
    , _logFile("")
    , _imageDirectory("")
    , _saveDirectory("")
Don Gagne's avatar
Don Gagne committed
159 160 161 162 163 164 165
{

}

void GeoTagWorker::run(void)
{
    _cancel = false;
166 167
    emit progressChanged(1);
    double nSteps = 5;
Andreas Bircher's avatar
Andreas Bircher committed
168

169 170
    // Load Images
    _imageList.clear();
Andreas Bircher's avatar
Andreas Bircher committed
171 172 173 174 175 176
    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);
177 178
    _imageList = imageDirectory.entryInfoList();
    if(_imageList.isEmpty()) {
Andreas Bircher's avatar
Andreas Bircher committed
179 180 181
        emit error(tr("The image directory doesn't contain images, make sure your images are of the JPG format"));
        return;
    }
182
    emit progressChanged((100/nSteps));
Andreas Bircher's avatar
Andreas Bircher committed
183

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

196
        _imageTime.append(exifParser.readTime(imageBuffer));
Andreas Bircher's avatar
Andreas Bircher committed
197

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

200 201 202 203 204
        if (_cancel) {
            qCDebug(GeotaggingLog) << "Tagging cancelled";
            emit error(tr("Tagging cancelled"));
            return;
        }
Andreas Bircher's avatar
Andreas Bircher committed
205 206
    }

207 208 209 210 211 212 213 214 215 216 217 218 219
    // 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;
220 221
    QString errorString;
    if (isULog) {
222
        ULogParser parser;
223
        parseComplete = parser.getTagsFromLog(log, _triggerList, errorString);
224 225 226 227 228 229 230 231

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

    }

    if (!parseComplete) {
232 233 234 235 236 237
        if (_cancel) {
            qCDebug(GeotaggingLog) << "Tagging cancelled";
            emit error(tr("Tagging cancelled"));
            return;
        } else {
            qCDebug(GeotaggingLog) << "Log parsing failed";
238 239
            errorString = tr("%1 - tagging cancelled").arg(errorString.isEmpty() ? tr("Log parsing failed") : errorString);
            emit error(errorString);
240 241
            return;
        }
Andreas Bircher's avatar
Andreas Bircher committed
242
    }
243
    emit progressChanged(3*(100/nSteps));
Andreas Bircher's avatar
Andreas Bircher committed
244

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

247 248 249 250 251
    if (_cancel) {
        qCDebug(GeotaggingLog) << "Tagging cancelled";
        emit error(tr("Tagging cancelled"));
        return;
    }
Andreas Bircher's avatar
Andreas Bircher committed
252

253
    // Filter Trigger
Andreas Bircher's avatar
Andreas Bircher committed
254
    if (!triggerFiltering()) {
255 256
        qCDebug(GeotaggingLog) << "Geotagging failed in trigger filtering";
        emit error(tr("Geotagging failed in trigger filtering"));
Andreas Bircher's avatar
Andreas Bircher committed
257 258
        return;
    }
259
    emit progressChanged(4*(100/nSteps));
Andreas Bircher's avatar
Andreas Bircher committed
260

261 262 263 264 265
    if (_cancel) {
        qCDebug(GeotaggingLog) << "Tagging cancelled";
        emit error(tr("Tagging cancelled"));
        return;
    }
Andreas Bircher's avatar
Andreas Bircher committed
266

267 268 269 270
    // Tag images
    int maxIndex = std::min(_imageIndices.count(), _triggerIndices.count());
    maxIndex = std::min(maxIndex, _imageList.count());
    for(int i = 0; i < maxIndex; i++) {
271 272 273 274 275
        int imageIndex = _imageIndices[i];
        if (imageIndex >= _imageList.count()) {
            emit error(tr("Geotagging failed. Image requested not present."));
            return;
        }
276 277 278 279 280 281 282
        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
283

284
        if (!exifParser.write(imageBuffer, _triggerList[_triggerIndices[i]])) {
285 286
            emit error(tr("Geotagging failed. Couldn't write to image."));
            return;
Andreas Bircher's avatar
Andreas Bircher committed
287
        } else {
288 289 290 291 292 293 294 295 296
            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
297
            }
298 299
            fileWrite.write(imageBuffer);
            fileWrite.close();
Andreas Bircher's avatar
Andreas Bircher committed
300
        }
301
        emit progressChanged(4*(100/nSteps) + ((100/nSteps) / maxIndex)*i);
Andreas Bircher's avatar
Andreas Bircher committed
302

Don Gagne's avatar
Don Gagne committed
303
        if (_cancel) {
304
            qCDebug(GeotaggingLog) << "Tagging cancelled";
Don Gagne's avatar
Don Gagne committed
305 306 307 308 309
            emit error(tr("Tagging cancelled"));
            return;
        }
    }

310 311 312 313 314 315
    if (_cancel) {
        qCDebug(GeotaggingLog) << "Tagging cancelled";
        emit error(tr("Tagging cancelled"));
        return;
    }

Don Gagne's avatar
Don Gagne committed
316 317
    emit progressChanged(100);
}
Andreas Bircher's avatar
Andreas Bircher committed
318 319 320

bool GeoTagWorker::triggerFiltering()
{
321 322
    _imageIndices.clear();
    _triggerIndices.clear();
323 324 325 326 327 328 329 330 331

    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
332 333
        _triggerIndices.append(i);
    }
334

Andreas Bircher's avatar
Andreas Bircher committed
335 336
    return true;
}