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

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

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

23 24 25
static const char* kTagged = "/TAGGED";

GeoTagController::GeoTagController()
Don Gagne's avatar
Don Gagne committed
26 27 28 29 30 31 32 33 34 35 36 37 38 39
    : _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()
{

}

40
void GeoTagController::setLogFile(QString filename)
Don Gagne's avatar
Don Gagne committed
41
{
42
    filename = QUrl(filename).toLocalFile();
Don Gagne's avatar
Don Gagne committed
43 44 45 46 47 48
    if (!filename.isEmpty()) {
        _worker.setLogFile(filename);
        emit logFileChanged(filename);
    }
}

49
void GeoTagController::setImageDirectory(QString dir)
Don Gagne's avatar
Don Gagne committed
50
{
51
    dir = QUrl(dir).toLocalFile();
Don Gagne's avatar
Don Gagne committed
52 53 54
    if (!dir.isEmpty()) {
        _worker.setImageDirectory(dir);
        emit imageDirectoryChanged(dir);
55 56 57 58 59 60 61
        if(_worker.saveDirectory() == "") {
            QDir saveDirectory = QDir(_worker.imageDirectory() + kTagged);
            if(saveDirectory.exists()) {
                _setErrorMessage(tr("Images have alreay been tagged. Existing images will be removed."));
                return;
            }
        }
Don Gagne's avatar
Don Gagne committed
62
    }
63 64
    _errorMessage.clear();
    emit errorMessageChanged(_errorMessage);
Don Gagne's avatar
Don Gagne committed
65 66
}

67
void GeoTagController::setSaveDirectory(QString dir)
68
{
69
    dir = QUrl(dir).toLocalFile();
70 71 72
    if (!dir.isEmpty()) {
        _worker.setSaveDirectory(dir);
        emit saveDirectoryChanged(dir);
73 74 75 76 77 78 79 80 81 82 83
        //-- Check and see if there are images already there
        QDir saveDirectory = QDir(_worker.saveDirectory());
        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()) {
            _setErrorMessage(tr("The save folder already contains images."));
            return;
        }
84
    }
85 86
    _errorMessage.clear();
    emit errorMessageChanged(_errorMessage);
87 88
}

89
void GeoTagController::startTagging()
Don Gagne's avatar
Don Gagne committed
90 91 92
{
    _errorMessage.clear();
    emit errorMessageChanged(_errorMessage);
93 94
    QDir imageDirectory = QDir(_worker.imageDirectory());
    if(!imageDirectory.exists()) {
95
        _setErrorMessage(tr("Cannot find the image directory."));
96 97 98
        return;
    }
    if(_worker.saveDirectory() == "") {
99 100
        QDir oldTaggedFolder = QDir(_worker.imageDirectory() + kTagged);
        if(oldTaggedFolder.exists()) {
101
            oldTaggedFolder.removeRecursively();
102
            if(!imageDirectory.mkdir(_worker.imageDirectory() + kTagged)) {
103
                _setErrorMessage(tr("Couldn't replace the previously tagged images"));
104 105 106 107 108 109
                return;
            }
        }
    } else {
        QDir saveDirectory = QDir(_worker.saveDirectory());
        if(!saveDirectory.exists()) {
110
            _setErrorMessage(tr("Cannot find the save directory."));
111 112 113
            return;
        }
    }
Don Gagne's avatar
Don Gagne committed
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
    _worker.start();
}

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

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

129 130 131 132 133 134 135

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

136
GeoTagWorker::GeoTagWorker()
Don Gagne's avatar
Don Gagne committed
137 138 139 140 141
    : _cancel(false)
{

}

142
void GeoTagWorker::run()
Don Gagne's avatar
Don Gagne committed
143 144
{
    _cancel = false;
145 146
    emit progressChanged(1);
    double nSteps = 5;
Andreas Bircher's avatar
Andreas Bircher committed
147

148 149
    // Load Images
    _imageList.clear();
Andreas Bircher's avatar
Andreas Bircher committed
150 151 152 153 154 155
    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);
156 157
    _imageList = imageDirectory.entryInfoList();
    if(_imageList.isEmpty()) {
Andreas Bircher's avatar
Andreas Bircher committed
158 159 160
        emit error(tr("The image directory doesn't contain images, make sure your images are of the JPG format"));
        return;
    }
161
    emit progressChanged((100/nSteps));
Andreas Bircher's avatar
Andreas Bircher committed
162

163 164
    // Parse EXIF
    ExifParser exifParser;
165
    _imageTime.clear();
166 167
    for (int i = 0; i < _imageList.size(); ++i) {
        QFile file(_imageList.at(i).absoluteFilePath());
Andreas Bircher's avatar
Andreas Bircher committed
168
        if (!file.open(QIODevice::ReadOnly)) {
169 170
            emit error(tr("Geotagging failed. Couldn't open an image."));
            return;
Andreas Bircher's avatar
Andreas Bircher committed
171
        }
172
        QByteArray imageBuffer = file.readAll();
Andreas Bircher's avatar
Andreas Bircher committed
173 174
        file.close();

175
        _imageTime.append(exifParser.readTime(imageBuffer));
Andreas Bircher's avatar
Andreas Bircher committed
176

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

179 180 181 182 183
        if (_cancel) {
            qCDebug(GeotaggingLog) << "Tagging cancelled";
            emit error(tr("Tagging cancelled"));
            return;
        }
Andreas Bircher's avatar
Andreas Bircher committed
184 185
    }

186 187 188 189 190 191 192 193 194 195 196 197 198
    // 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;
199 200
    QString errorString;
    if (isULog) {
201
        ULogParser parser;
202
        parseComplete = parser.getTagsFromLog(log, _triggerList, errorString);
203 204 205 206 207 208 209 210

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

    }

    if (!parseComplete) {
211 212 213 214 215 216
        if (_cancel) {
            qCDebug(GeotaggingLog) << "Tagging cancelled";
            emit error(tr("Tagging cancelled"));
            return;
        } else {
            qCDebug(GeotaggingLog) << "Log parsing failed";
217 218
            errorString = tr("%1 - tagging cancelled").arg(errorString.isEmpty() ? tr("Log parsing failed") : errorString);
            emit error(errorString);
219 220
            return;
        }
Andreas Bircher's avatar
Andreas Bircher committed
221
    }
222
    emit progressChanged(3*(100/nSteps));
Andreas Bircher's avatar
Andreas Bircher committed
223

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

226 227 228 229 230
    if (_cancel) {
        qCDebug(GeotaggingLog) << "Tagging cancelled";
        emit error(tr("Tagging cancelled"));
        return;
    }
Andreas Bircher's avatar
Andreas Bircher committed
231

232
    // Filter Trigger
Andreas Bircher's avatar
Andreas Bircher committed
233
    if (!triggerFiltering()) {
234 235
        qCDebug(GeotaggingLog) << "Geotagging failed in trigger filtering";
        emit error(tr("Geotagging failed in trigger filtering"));
Andreas Bircher's avatar
Andreas Bircher committed
236 237
        return;
    }
238
    emit progressChanged(4*(100/nSteps));
Andreas Bircher's avatar
Andreas Bircher committed
239

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

246 247 248 249
    // Tag images
    int maxIndex = std::min(_imageIndices.count(), _triggerIndices.count());
    maxIndex = std::min(maxIndex, _imageList.count());
    for(int i = 0; i < maxIndex; i++) {
250 251
        int imageIndex = _imageIndices[i];
        if (imageIndex >= _imageList.count()) {
252
            emit error(tr("Geotagging failed. Requesting image #%1, but only %2 images present.").arg(imageIndex).arg(_imageList.count()));
253 254
            return;
        }
255 256 257 258 259 260 261
        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
262

263
        if (!exifParser.write(imageBuffer, _triggerList[_triggerIndices[i]])) {
264 265
            emit error(tr("Geotagging failed. Couldn't write to image."));
            return;
Andreas Bircher's avatar
Andreas Bircher committed
266
        } else {
267 268 269 270 271 272 273 274 275
            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
276
            }
277 278
            fileWrite.write(imageBuffer);
            fileWrite.close();
Andreas Bircher's avatar
Andreas Bircher committed
279
        }
280
        emit progressChanged(4*(100/nSteps) + ((100/nSteps) / maxIndex)*i);
Andreas Bircher's avatar
Andreas Bircher committed
281

Don Gagne's avatar
Don Gagne committed
282
        if (_cancel) {
283
            qCDebug(GeotaggingLog) << "Tagging cancelled";
Don Gagne's avatar
Don Gagne committed
284 285 286 287 288
            emit error(tr("Tagging cancelled"));
            return;
        }
    }

289 290 291 292 293 294
    if (_cancel) {
        qCDebug(GeotaggingLog) << "Tagging cancelled";
        emit error(tr("Tagging cancelled"));
        return;
    }

Don Gagne's avatar
Don Gagne committed
295 296
    emit progressChanged(100);
}
Andreas Bircher's avatar
Andreas Bircher committed
297 298 299

bool GeoTagWorker::triggerFiltering()
{
300 301
    _imageIndices.clear();
    _triggerIndices.clear();
302 303 304 305 306 307
    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++) {
308
        _imageIndices.append(static_cast<int>(_triggerList[i].imageSequence));
Andreas Bircher's avatar
Andreas Bircher committed
309 310 311 312
        _triggerIndices.append(i);
    }
    return true;
}