Skip to content
Snippets Groups Projects
petrack.cpp 145 KiB
Newer Older
/*
 * PeTrack - Software for tracking pedestrians movement in videos
 * Copyright (C) 2023 Forschungszentrum Jülich GmbH, IAS-7
 *
 * 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 <https://www.gnu.org/licenses/>.
 */

d.kilic's avatar
d.kilic committed
#include <QSignalMapper>
#include <QtOpenGL>
#include <QtWidgets>
d.kilic's avatar
d.kilic committed

// Added for Qt5 support
#include "IO.h"
#include "aboutDialog.h"
#include "animation.h"
#include "autoCalib.h"
#include "autosaveSettings.h"
#include "backgroundItem.h"
#include "calibFilter.h"
#include "codeMarkerWidget.h"
d.kilic's avatar
d.kilic committed
#include "colorMarkerItem.h"
#include "colorMarkerWidget.h"
#include "colorRangeWidget.h"
d.kilic's avatar
d.kilic committed
#include "control.h"
#include "coordItem.h"
#include "coordinateSystemBox.h"
#include "filterBeforeBox.h"
d.kilic's avatar
d.kilic committed
#include "gridItem.h"
d.kilic's avatar
d.kilic committed
#include "imageItem.h"
#include "logger.h"
d.kilic's avatar
d.kilic committed
#include "logoItem.h"
#include "moCapItem.h"
#include "multiColorMarkerItem.h"
#include "multiColorMarkerWidget.h"
#include "openMoCapDialog.h"
#include "pMessageBox.h"
#include "person.h"
#include "petrack.h"
#include "player.h"
#include "stereoItem.h"
#include "stereoWidget.h"
d.kilic's avatar
d.kilic committed
#include "tracker.h"
#include "trackerItem.h"
d.kilic's avatar
d.kilic committed
#include "trackerReal.h"
#include "view.h"
#include "worldImageCorrespondence.h"
#include <QtPrintSupport/QPrintDialog>
#include <QtPrintSupport/QPrinter>
#include <cmath>
Schrödter, Tobias's avatar
Schrödter, Tobias committed
#include <ctime>
d.kilic's avatar
d.kilic committed
#include <iomanip>
Schrödter, Tobias's avatar
Schrödter, Tobias committed
#include <opencv2/opencv.hpp>
d.kilic's avatar
d.kilic committed

d.kilic's avatar
d.kilic committed
int Petrack::trcVersion = 0;

// Reihenfolge des anlegens der objekte ist sehr wichtig
Petrack::Petrack(QString petrackVersion) :
    mExtrCalibration(mPersonStorage),
    mPetrackVersion(std::move(petrackVersion)),
    mAuthors(IO::readAuthors(QCoreApplication::applicationDirPath() + "/.zenodo.json"))
d.kilic's avatar
d.kilic committed
{
    QIcon icon;
    icon.addFile(":/icon");          // about
d.kilic's avatar
d.kilic committed
    icon.addFile(":/icon_smallest"); // window title bar
    setWindowIcon(icon);
    mHeadSize            = -1;
    mCmPerPixel          = -1;
    mScene               = nullptr;
    mTracker             = nullptr;
    mTrackerReal         = nullptr; // damit beim zeichnen von control mit analysePlot nicht auf einen feheler laeuft
    mStatusLabelFPS      = nullptr;
    mStatusPosRealHeight = nullptr;
    mStatusLabelPosReal  = nullptr;
    mImageItem           = nullptr;
    mRecognitionChanged  = true;
    mTrackChanged        = true;
    mCoordItem           = nullptr;
    mImage               = nullptr;
d.kilic's avatar
d.kilic committed
    setLoading(true);

    setAcceptDrops(true);

    int space = 2;

    mBrightContrastFilter.disable();
    mBorderFilter.disable();
    mSwapFilter.disable();
    mBackgroundFilter.disable();
    mStereoContext = nullptr;
    mCalibFilter.disable();
    mScene = new QGraphicsScene(this);

    mTrackingRoiItem = new RoiItem(this, Qt::blue);
    connect(mTrackingRoiItem, &RoiItem::changed, this, [=]() { this->setTrackChanged(true); });
    mTrackingRoiItem->setZValue(4); // groesser heisst weiter oben

    mRecognitionRoiItem = new RoiItem(this, Qt::green);
    connect(mRecognitionRoiItem, &RoiItem::changed, this, [=]() { this->setRecognitionChanged(true); });
    mRecognitionRoiItem->setZValue(5); // groesser heisst weiter oben


    // setup control

    auto updateImageCallback = [this]()
    {
        if(!isLoading())
        {
            updateImage();
        }
    };

    auto updateStatusPos = [this]() { setStatusPosReal(); };
    auto updateHeadSize  = [this]() { setHeadSize(); };
    auto getBorderSize   = [this]() { return getImageBorderSize(); };

    auto *filterBeforeBox = new FilterBeforeBox(
        nullptr, // reparented when added to layout
        *getBackgroundFilter(),
        *getBrightContrastFilter(),
        *getBorderFilter(),
        *getSwapFilter(),
        updateImageCallback);

    auto *intrinsicBox = new IntrinsicBox(this, *getAutoCalib(), *getCalibFilter(), updateImageCallback);
    auto *extrinsicBox = new ExtrinsicBox(this, *getExtrCalibration());
    mImageItem         = new ImageItem(this, nullptr);
    auto *coordSysBox  = new CoordinateSystemBox(
        this,
        updateStatusPos,
        updateHeadSize,
        getBorderSize,
        *intrinsicBox,
        *extrinsicBox,
        *mImageItem,
        mExtrCalibration);

    mControlWidget = new Control(
        *this,
        *mScene,
        mReco,
        *mTrackingRoiItem,
        *mRecognitionRoiItem,
        mMissingFrames,
        filterBeforeBox,
        intrinsicBox,
        extrinsicBox,
        coordSysBox);

    connect(mImageItem, &ImageItem::imageChanged, mControlWidget, &Control::imageSizeChanged);

    // end setup control
d.kilic's avatar
d.kilic committed

    mWorldImageCorrespondence = &mControlWidget->getWorldImageCorrespondence();

d.kilic's avatar
d.kilic committed
    mStereoWidget = new StereoWidget(this);
    mStereoWidget->setWindowFlags(Qt::Window);
    mStereoWidget->setWindowTitle("Stereo parameter");

    mColorRangeWidget = new ColorRangeWidget(this);
    mColorRangeWidget->setWindowFlags(Qt::Window);
    mColorRangeWidget->setWindowTitle("Color range");

    mColorMarkerWidget = new ColorMarkerWidget(this);
    mColorMarkerWidget->setWindowFlags(Qt::Window);
    mColorMarkerWidget->setWindowTitle("Color marker parameter");

    mCodeMarkerWidget = new CodeMarkerWidget(this, mReco.getCodeMarkerOptions(), nullptr);
d.kilic's avatar
d.kilic committed
    mCodeMarkerWidget->setWindowFlags(Qt::Window);
    mCodeMarkerWidget->setWindowTitle("Code marker parameter");


    mMultiColorMarkerWidget = new MultiColorMarkerWidget(this);
    mMultiColorMarkerWidget->setWindowFlags(Qt::Window);
    mMultiColorMarkerWidget->setWindowTitle("MultiColor marker parameter");

    mAnimation = new Animation(this);

    mLogoItem = new LogoItem(this); // durch uebergabe von scene wird indirekt ein scene->addItem() aufgerufen
    mLogoItem->setZValue(6);        // groesser heisst weiter oben
d.kilic's avatar
d.kilic committed

    mExtrCalibration.setMainWindow(this);

    mGridItem = new GridItem(this, nullptr, coordSysBox);
d.kilic's avatar
d.kilic committed
    mGridItem->setZValue(2.5); // durch uebergabe von scene wird indirekt ein scene->addItem() aufgerufen

    mCoordItem = new CoordItem(this, nullptr, coordSysBox);
d.kilic's avatar
d.kilic committed
    mCoordItem->setZValue(3); // groesser heisst weiter oben

    mViewWidget = new ViewWidget(this);
    mView       = mViewWidget->view();
d.kilic's avatar
d.kilic committed
    mView->setScene(mScene);
    connect(mView, &GraphicsView::mouseDoubleClick, this, [this]() { this->openSequence(); });
    connect(mView, &GraphicsView::mouseShiftDoubleClick, this, &Petrack::addManualTrackPointOnlyVisible);
    connect(mView, &GraphicsView::mouseShiftControlDoubleClick, this, &Petrack::splitTrackPerson);
    connect(mView, &GraphicsView::mouseControlDoubleClick, this, &Petrack::addOrMoveManualTrackPoint);
    connect(mView, &GraphicsView::mouseRightDoubleClick, this, &Petrack::deleteTrackPoint);
    connect(mView, &GraphicsView::mouseMiddleDoubleClick, this, &Petrack::deleteTrackPointAll);
    connect(mView, &GraphicsView::mouseShiftWheel, this, &Petrack::skipToFrameWheel);
    connect(mView, &GraphicsView::mouseCtrlAltDoubleClick, this, &Petrack::skipToFrameFromTrajectory);
    connect(mView, &GraphicsView::mouseAltMoved, this, &Petrack::moveTrackPoint);
    connect(mView, &GraphicsView::mouseAltPressed, this, &Petrack::selectPersonForMoveTrackPoint);
    connect(mView, &GraphicsView::altReleased, this, &Petrack::releaseTrackPoint);
    connect(mView, &GraphicsView::mouseAltReleased, this, &Petrack::releaseTrackPoint);
    connect(mView, &GraphicsView::mouseCtrlWheel, this, &Petrack::scrollShowOnly);
d.kilic's avatar
d.kilic committed

    mLogWindow = new LogWindow(this, nullptr);
    mLogWindow->setWindowFlags(Qt::Window);
    mLogWindow->setWindowTitle("Log");

d.kilic's avatar
d.kilic committed
    mPlayerWidget = new Player(mAnimation, this);

    QVBoxLayout *vLayout = new QVBoxLayout;
    vLayout->setSpacing(space);
    vLayout->addWidget(mViewWidget);
    vLayout->addWidget(mPlayerWidget);

    //---------------------------

    mTracker     = new Tracker(this, mPersonStorage);
    mTrackerReal = new TrackerReal(this, mPersonStorage);
    mTrackerItem = new TrackerItem(this, mPersonStorage);
d.kilic's avatar
d.kilic committed
    mTrackerItem->setZValue(5); // groesser heisst weiter oben

    mControlWidget->getColorPlot()->setPersonStorage(&mPersonStorage);
d.kilic's avatar
d.kilic committed
#ifdef QWT
    mControlWidget->getAnalysePlot()->setTrackerReal(mTrackerReal);
#endif

    //---------------------------

    mStereoItem = new StereoItem(this);
    mStereoItem->setZValue(2); // groesser heisst weiter oben
d.kilic's avatar
d.kilic committed
    mStereoItem->setVisible(false);

    //---------------------------

    mColorMarkerItem = new ColorMarkerItem(this);
    mColorMarkerItem->setZValue(2); // groesser heisst weiter oben
d.kilic's avatar
d.kilic committed
    mColorMarkerItem->setVisible(false);
    //---------------------------

    mCodeMarkerItem = new CodeMarkerItem(this, mReco.getCodeMarkerOptions());
    mCodeMarkerItem->setZValue(2); // groesser heisst weiter oben
d.kilic's avatar
d.kilic committed
    mCodeMarkerItem->setVisible(false);
    //---------------------------

    mMultiColorMarkerItem = new MultiColorMarkerItem(this);
    mMultiColorMarkerItem->setZValue(2); // groesser heisst weiter oben
d.kilic's avatar
d.kilic committed
    mMultiColorMarkerItem->setVisible(false);

    //---------------------------

    mBackgroundItem = new BackgroundItem(this, nullptr, *filterBeforeBox);
d.kilic's avatar
d.kilic committed
    mBackgroundItem->setZValue(2.2); // um so groesser um so hoeher um so eher zu sehen
    mBackgroundItem->setVisible(false);

    //---------------------------

    mMoCapItem = new MoCapItem(*this, *mAnimation, mMoCapController);
    mMoCapItem->setZValue(3); // um so groesser um so hoeher um so eher zu sehen

d.kilic's avatar
d.kilic committed
    /// Add Items
    mScene->addItem(mImageItem);
    mScene->addItem(mLogoItem);
    mScene->addItem(mGridItem);
    mScene->addItem(mCoordItem);
    mScene->addItem(mTrackingRoiItem);
    mScene->addItem(mRecognitionRoiItem);
    mScene->addItem(mTrackerItem);
    mScene->addItem(mStereoItem);
    mScene->addItem(mColorMarkerItem);
    mScene->addItem(mCodeMarkerItem);
    mScene->addItem(mMultiColorMarkerItem);
    mScene->addItem(mBackgroundItem);
    mScene->addItem(mMoCapItem);
d.kilic's avatar
d.kilic committed

    //---------------------------

    mCentralLayout = new QHBoxLayout;
    mCentralLayout->setMargin(space);
    mCentralWidget = new QFrame;
    mCentralWidget->setFrameStyle(QFrame::Sunken | QFrame::StyledPanel);
    mCentralWidget->setLayout(mCentralLayout);
    setCentralWidget(mCentralWidget);
    mSplitter = new QSplitter(this);
    // create playAndView-Widget to wrap layout, since QSplitter only accepts widgets
    QWidget *playAndView = new QWidget(this);
    playAndView->setLayout(vLayout);
    mSplitter->addWidget(playAndView);

    mSplitter->addWidget(mControlWidget);

    mSplitter->setStretchFactor(0, 1);
    mSplitter->setStretchFactor(1, 0);

    mCentralLayout->addWidget(mSplitter);


d.kilic's avatar
d.kilic committed
    setWindowTitle(tr("PeTrack"));

    //---------------------------
    mAutoCalib.setMainWindow(this);
    //---------------------------

    createActions();
    createMenus();
    createStatusBar();

    auto *exportShortCut = new QShortcut{QKeySequence("Ctrl+e"), this};
    connect(exportShortCut, &QShortcut::activated, this, [=]() { exportTracker(); });

    auto *toggleOnlineTracking = new QShortcut{QKeySequence("Shift+t"), this};
    connect(toggleOnlineTracking, &QShortcut::activated, this, [=]() { mControlWidget->toggleOnlineTracking(); });
    // TODO delete once we get Options to be value only (i.e. no Pointer/Ref anymore)
    mReco.getCodeMarkerOptions().setControlWidget(mControlWidget);
    mReco.getCodeMarkerOptions().setCodeMarkerItem(mCodeMarkerItem);
    mSeqFileName = QDir::currentPath(); // fuer allerersten Aufruf des Programms
d.kilic's avatar
d.kilic committed
    readSettings();

    saveXml(mDefaultSettings); // noch nicht noetig, da eh noch nicht fkt
d.kilic's avatar
d.kilic committed

    mShowFPS = 0;

    mTrcFileName = "";

    // initialer Aufruf, damit beim reinen Laden einer Videodatei die Defaultwerte in control genommen werden zum Setzen
    setHeadSize();

    // um im background subtraction filter das hoehenbild zu beruecksichtigen
    mBackgroundFilter.setStereoContext(&mStereoContext);

    mAutoBackTrack          = true;  // ist der default, dann wenn in XML-Datei nicht drin steht
d.kilic's avatar
d.kilic committed
    mAutoTrackOptimizeColor = false; // ist der default, dann wenn in XML-Datei nicht drin steht

    setLoading(false);
}
Petrack::~Petrack()
{
    delete mImage;
    // hier muessten weitere stehen insb die im konstruktor erzeugt werden
    // aber da petrack nur vernichtet wird, wenn programm beendet wird, kann man sich das auch schenken
}

void Petrack::dragEnterEvent(QDragEnterEvent *event)
{
    if(event->mimeData()->hasUrls())
d.kilic's avatar
d.kilic committed
        event->acceptProposedAction();
d.kilic's avatar
d.kilic committed
}

/**
 * @brief Accepts dropped .pet, .trc and media files
 *
 * Opens the project for a .pet. Imports the trajectories for a .trc
 * and tries to open the sequence for any other kind of file.
 *
 * @param event
 */
d.kilic's avatar
d.kilic committed
void Petrack::dropEvent(QDropEvent *event)
{
    if(event->mimeData()->hasUrls())
d.kilic's avatar
d.kilic committed
    {
        if(event->mimeData()->urls().first().toLocalFile().endsWith(".pet", Qt::CaseInsensitive))
d.kilic's avatar
d.kilic committed
            openProject(event->mimeData()->urls().first().toLocalFile());
        else if(event->mimeData()->urls().first().toLocalFile().endsWith(".trc", Qt::CaseInsensitive))
d.kilic's avatar
d.kilic committed
            importTracker(event->mimeData()->urls().first().toLocalFile());
d.kilic's avatar
d.kilic committed
        else
d.kilic's avatar
d.kilic committed
            openSequence(event->mimeData()->urls().first().toLocalFile());
d.kilic's avatar
d.kilic committed
        event->acceptProposedAction();
    }
}

void Petrack::updateSceneRect()
{
    double iW = 0, iH = 0, bS = 0;
d.kilic's avatar
d.kilic committed

    if(mImage && !mImage->isNull())
d.kilic's avatar
d.kilic committed
    {
        iW = mImage->width();
        iH = mImage->height();
        bS = getImageBorderSize();
    }
    else
    {
        mScene->setSceneRect(mScene->itemsBoundingRect());
        return;
    }
d.kilic's avatar
d.kilic committed

    if(mControlWidget->getCalibCoordShow())
d.kilic's avatar
d.kilic committed
    {
        double scale = mControlWidget->getCalibCoordScale() / 10.;
        auto   t     = mControlWidget->getCalibCoord2DTrans() / 10.;
d.kilic's avatar
d.kilic committed

        // setzen der bounding box der scene
        // Faktor 1.1 dient dazu, dass auch Zahl "1" bei coord gut in sichtbaren Bereich passt
        double xMin = (t.x() - 1.1 * scale < -bS) ? t.x() - 1.1 * scale : -bS;
        double yMin = (t.y() - 1.1 * scale < -bS) ? t.y() - 1.1 * scale : -bS;
        double xMax = (t.x() + 1.1 * scale > iW - bS) ? t.x() + 1.1 * scale : iW - bS;
        double yMax = (t.y() + 1.1 * scale > iH - bS) ? t.y() + 1.1 * scale : iH - bS;
        mScene->setSceneRect(xMin, yMin, xMax - xMin, yMax - yMin);
d.kilic's avatar
d.kilic committed
    }
    else
d.kilic's avatar
d.kilic committed
        mScene->setSceneRect(-bS, -bS, iW, iH);
d.kilic's avatar
d.kilic committed
}

/**
 * @brief Loads the content of a .pet file into petrack
 *
 * @param doc the DOM of the .pet file
 * @param openSeq true, if sequence given in doc should be opened
 */
d.kilic's avatar
d.kilic committed
void Petrack::openXml(QDomDocument &doc, bool openSeq)
{
    mMissingFrames.reset();
    bool                      missingFramesExecuted = false;
    std::vector<MissingFrame> missingFrames{};

d.kilic's avatar
d.kilic committed
    QDomElement root = doc.firstChildElement("PETRACK");
    QString     seq;
    int         frame = -1, sourceFrameIn = -1, sourceFrameOut = -1;
    double      fps              = DEFAULT_FPS;
    int         onlyPeopleNr     = 1;
    QString     onlyPeopleNrList = "1";
    int         zoom = 250, rotate = 0, hScroll = 0, vScroll = 0;
d.kilic's avatar
d.kilic committed
    enum Camera cam = cameraUnset;
    setLoading(true);
    auto petVersion = root.attribute("VERSION");

d.kilic's avatar
d.kilic committed
    for(QDomElement elem = root.firstChildElement(); !elem.isNull(); elem = elem.nextSiblingElement())
    {
        if(elem.tagName() == "MAIN")
d.kilic's avatar
d.kilic committed
        {
            if(elem.hasAttribute("SRC"))
d.kilic's avatar
d.kilic committed
            {
                seq            = elem.attribute("SRC");
d.kilic's avatar
d.kilic committed
                QString tmpSeq = getExistingFile(seq, mProFileName);
                if(tmpSeq != "")
d.kilic's avatar
d.kilic committed
                    seq = tmpSeq;

                // will show undistorted image until calibration is loaded as well
                // but image changes maximum values e.g. for cx; need to set this first to ensure that the
                // correct values can be read in
                mSeqFileName = seq;
                if(openSeq)
                {
                    if(seq != "")
                    {
                        openSequence(seq);
                    }
                    else
                    {
                        mAnimation->reset();
                        mImg         = cv::Mat();
                        mImgFiltered = cv::Mat();
                        delete mImage;
                        mImage = nullptr;
                        updateSequence();
                        mLogoItem->ensureVisible();
                        mLogoItem->fadeIn();
                    }
                }
d.kilic's avatar
d.kilic committed
            }
            if(elem.hasAttribute("STATUS_HEIGHT"))
d.kilic's avatar
d.kilic committed
            {
                if(mStatusPosRealHeight) // null kann eigentlich nicht vorkommen, da im constructor von petrack erzeugt
                                         // wird
d.kilic's avatar
d.kilic committed
                    mStatusPosRealHeight->setValue(elem.attribute("STATUS_HEIGHT").toDouble());
d.kilic's avatar
d.kilic committed
            }
        }
        else if(elem.tagName() == "STEREO")
d.kilic's avatar
d.kilic committed
        {
            mStereoWidget->getXml(elem);
        }
        else if(elem.tagName() == "COLOR_MARKER")
d.kilic's avatar
d.kilic committed
        {
            mColorMarkerWidget->getXml(elem);
        }
        else if(elem.tagName() == "CODE_MARKER")
d.kilic's avatar
d.kilic committed
        {
            mCodeMarkerWidget->getXml(elem);
        }
        else if(elem.tagName() == "MULTI_COLOR_MARKER")
d.kilic's avatar
d.kilic committed
        {
            mMultiColorMarkerWidget->getXml(elem);
        }
        else if(elem.tagName() == "MOCAP")
        {
            mMoCapController.getXml(elem);
        }
        else if(elem.tagName() == "CONTROL")
d.kilic's avatar
d.kilic committed
        {
            mControlWidget->getXml(elem, petVersion);
d.kilic's avatar
d.kilic committed
            QDomElement tmpElem = (elem.firstChildElement("TRACKING")).firstChildElement("PATH");
            if(tmpElem.hasAttribute("ONLY_PEOPLE_NR"))
d.kilic's avatar
d.kilic committed
                onlyPeopleNr = tmpElem.attribute("ONLY_PEOPLE_NR").toInt();
            if(tmpElem.hasAttribute("ONLY_PEOPLE_NR_LIST"))
d.kilic's avatar
d.kilic committed
                onlyPeopleNrList = tmpElem.attribute("ONLY_PEOPLE_NR_LIST");
d.kilic's avatar
d.kilic committed
        }
        else if(elem.tagName() == "EXTR_CALIBRATION")
        else if(elem.tagName() == "PLAYER")
d.kilic's avatar
d.kilic committed
        {
            if(elem.hasAttribute("FRAME"))
d.kilic's avatar
d.kilic committed
            {
                frame = elem.attribute("FRAME").toInt();
            }
            if(elem.hasAttribute("FPS"))
d.kilic's avatar
d.kilic committed
            {
                fps = elem.attribute("FPS").toDouble();
            }
            if(elem.hasAttribute("SOURCE_FRAME_IN"))
d.kilic's avatar
d.kilic committed
            {
                sourceFrameIn = elem.attribute("SOURCE_FRAME_IN").toInt();
            }
            if(elem.hasAttribute("SOURCE_FRAME_OUT"))
d.kilic's avatar
d.kilic committed
            {
                sourceFrameOut = elem.attribute("SOURCE_FRAME_OUT").toInt();
            }
            if(elem.hasAttribute("PLAYER_SPEED_FIXED"))
d.kilic's avatar
d.kilic committed
            {
                mPlayerWidget->setPlayerSpeedLimited(elem.attribute("PLAYER_SPEED_FIXED").toInt());
d.kilic's avatar
d.kilic committed
            }
        }
        else if(elem.tagName() == "VIEW")
d.kilic's avatar
d.kilic committed
        {
            if(elem.hasAttribute("ANTIALIAS"))
d.kilic's avatar
d.kilic committed
            {
                mAntialiasAct->setChecked(elem.attribute("ANTIALIAS").toInt() == Qt::Checked);
d.kilic's avatar
d.kilic committed
            }
            if(elem.hasAttribute("OPENGL"))
d.kilic's avatar
d.kilic committed
            {
                mOpenGLAct->setChecked(elem.attribute("OPENGL").toInt() == Qt::Checked);
d.kilic's avatar
d.kilic committed
            }
            if(elem.hasAttribute("SAVE_TRANSFORMED"))
d.kilic's avatar
d.kilic committed
            {
                mCropZoomViewAct->setChecked(elem.attribute("SAVE_TRANSFORMED") == Qt::Checked);
d.kilic's avatar
d.kilic committed
            }
            if(elem.hasAttribute("TRANSFORMATION"))
d.kilic's avatar
d.kilic committed
            {
                QString     matStr = elem.attribute("TRANSFORMATION");
d.kilic's avatar
d.kilic committed
                QTextStream in(&matStr);
                in >> zoom >> rotate >> hScroll >> vScroll;
            }
            if(elem.hasAttribute("CAMERA"))
d.kilic's avatar
d.kilic committed
            {
                cam = (enum Camera) elem.attribute("CAMERA").toInt();
            }
            if(elem.hasAttribute("HIDE_CONTROLS"))
d.kilic's avatar
d.kilic committed
            {
                mHideControlsAct->setChecked(elem.attribute("HIDE_CONTROLS").toInt() == Qt::Checked);
d.kilic's avatar
d.kilic committed
            }
        }
        else if(elem.tagName() == "AUTO_TRACK")
d.kilic's avatar
d.kilic committed
        {
            if(elem.hasAttribute("BACK_TRACK"))
d.kilic's avatar
d.kilic committed
            {
                mAutoBackTrack = elem.attribute("BACK_TRACK").toInt();
            }
            if(elem.hasAttribute("OPTIMZE_COLOR"))
d.kilic's avatar
d.kilic committed
            {
                mAutoTrackOptimizeColor = elem.attribute("OPTIMZE_COLOR").toInt();
            }
        }
        else if(elem.tagName() == "MISSING_FRAMES")
        {
            if((elem.hasAttribute("executed")) && (elem.attribute("executed").toInt() == 1))
            {
                missingFramesExecuted = true;
                auto node             = elem.firstChildElement("FRAME");
                for(; !node.isNull(); node = node.nextSiblingElement("FRAME"))
                {
                    size_t num   = node.attribute("NUM_FRAME").toUInt();
                    int    count = node.attribute("NUM_MISSING").toInt();
                    missingFrames.push_back(MissingFrame{num, count});
                }
            }
        }
d.kilic's avatar
d.kilic committed
        else
            SPDLOG_ERROR("Unknown PETRACK tag {}", elem.tagName());
d.kilic's avatar
d.kilic committed
    }

    mMissingFrames.setExecuted(missingFramesExecuted);
    mMissingFrames.setMissingFrames(missingFrames);

d.kilic's avatar
d.kilic committed
    mViewWidget->setZoomLevel(zoom);
    mViewWidget->setRotateLevel(rotate);
    mView->horizontalScrollBar()->setValue(hScroll);
    mView->verticalScrollBar()->setValue(vScroll);

    bool loaded = false;
    if(!mBackgroundFilter.getFilename().isEmpty())
d.kilic's avatar
d.kilic committed
    {
        if(!(loaded = mBackgroundFilter.load(mBackgroundFilter.getFilename())))
            SPDLOG_ERROR("Error: loading background file {}!", mBackgroundFilter.getFilename());
d.kilic's avatar
d.kilic committed
    }

    mPlayerWidget->setFrameInNum(sourceFrameIn == -1 ? mAnimation->getSourceInFrameNum() : sourceFrameIn);
    mPlayerWidget->setFrameOutNum(sourceFrameOut == -1 ? mAnimation->getSourceOutFrameNum() : sourceFrameOut);
d.kilic's avatar
d.kilic committed
    mPlayerWidget->update();

    if(frame != -1)
d.kilic's avatar
d.kilic committed
    {
        // used first loaded image to determine bg, should not have happened
        if(mControlWidget->isFilterBgChecked() && !loaded)
            // reset background and first skip to selected frame
            mBackgroundFilter.reset();
        // will call updateImage and update bg
        mPlayerWidget->skipToFrame(frame);
d.kilic's avatar
d.kilic committed
    }
    else if(loaded)
d.kilic's avatar
d.kilic committed
        updateImage();
d.kilic's avatar
d.kilic committed

    // nicht schon in control, sonst loescht opensequence wieder tracker
    if(mTrcFileName != "")
d.kilic's avatar
d.kilic committed
    {
        // vorher loeschen aller trajektorien, da sonst nach start im ersten bild
        // mgl zwei trackpoints
        // beim haendischen importieren sind weiterhin parallele trajektorien moeglich (warnung wird ausgegeben)
        frame = 0; // default
        if((mPersonStorage.largestLastFrame() >= frame) && (getPersonStorage().smallestFirstFrame() <= frame))
d.kilic's avatar
d.kilic committed
        {
            mPersonStorage.clear();
d.kilic's avatar
d.kilic committed
            mTracker->reset();
        }
        importTracker(mTrcFileName);
    }

    mControlWidget->setTrackShowOnlyNr(onlyPeopleNr);
    mControlWidget->trackShowOnlyNrList()->setText(onlyPeopleNrList);
d.kilic's avatar
d.kilic committed

    if(cam == cameraLeft)
d.kilic's avatar
d.kilic committed
        mCameraLeftViewAct->setChecked(true);
    else if(cam == cameraRight)
d.kilic's avatar
d.kilic committed
        mCameraRightViewAct->setChecked(true);
d.kilic's avatar
d.kilic committed
    setCamera();
    mPlayerWidget->setFPS(fps); // erst spaet setzen, damit Wert den des geladenen Videos ueberschreiben kann
    updateImage();              // needed to undistort, draw border, etc. for first display
d.kilic's avatar
d.kilic committed
    setLoading(false);
}

/**
 * Open a petrack project.
 * When mProFileName is set and a file, this method will ask to save the project before opening the new one.
 * If it is not a file, the path is used as directory for the filedialog.
 *
 * If the given filename is empty, a dialog opens that asks for a file to open.
 *
 * @param fileName the filename to open
 * @param openSeq  true, if sequence given in project should be opened
 */
d.kilic's avatar
d.kilic committed
void Petrack::openProject(QString fileName, bool openSeq) // default fileName="", openSequence = true
{
    if(QFileInfo(mProFileName).isFile()) // a project is already loaded
        if(!maybeSave())
d.kilic's avatar
d.kilic committed
            return;
    else if(!QFileInfo(mProFileName).isDir())
    {
        SPDLOG_INFO("mProFileName neither file nor directory - resetting");
        mProFileName = "";
    }
d.kilic's avatar
d.kilic committed
    // if no destination file or folder is given
    if(fileName.isEmpty())
        fileName = QFileDialog::getOpenFileName(
            this,
            tr("Select project file"),
            QFileInfo(mProFileName).path(),
            tr("PeTrack project file (*.pet);;All files (*.*)"));
    if(Autosave::autosaveExists(fileName) && fileName != mProFileName)
    {
        auto ret = PQuestion(this, "Autosave detected", "An autosave was detected.\nDo you want to load the Autosave?");
        if(ret == PMessageBox::StandardButton::Yes)
        {
            setProFileName(fileName);
            mAutosave.loadAutosave();
            return;
        }
    }

    if(!fileName.isEmpty())
d.kilic's avatar
d.kilic committed
    {
        QFile file(fileName);
        if(!file.open(QIODevice::ReadOnly))
d.kilic's avatar
d.kilic committed
        {
            PCritical(this, tr("PeTrack"), tr("Cannot open %1:\n%2.").arg(fileName, file.errorString()));
d.kilic's avatar
d.kilic committed
            return;
        }

        QDomDocument oldSettings;
        QString      oldProFilename = mProFileName;
        saveXml(oldSettings);

d.kilic's avatar
d.kilic committed
        resetSettings();
        QDomDocument doc("PETRACK"); // eigentlich Pfad zu Beschreibungsdatei fuer Dateiaufbau
        if(!doc.setContent(&file))
d.kilic's avatar
d.kilic committed
        {
            PCritical(this, tr("PeTrack"), tr("Cannot read content from %1.").arg(fileName));
d.kilic's avatar
d.kilic committed
            file.close();
            return;
        }

        SPDLOG_INFO("open: {}", fileName);
d.kilic's avatar
d.kilic committed
        file.close();
        setProFileName(fileName);
d.kilic's avatar
d.kilic committed

        QDomElement root = doc.firstChildElement("PETRACK");
        if(root.hasAttribute("VERSION"))
            if(root.attribute("VERSION") != mPetrackVersion)
d.kilic's avatar
d.kilic committed
            {
                PWarning(
                    this,
                    tr("PeTrack"),
                    tr("Reading %1:\nDifferent version numbers %2 (application) and %3 (file) may cause problems.")
                        .arg(fileName, mPetrackVersion, root.attribute("VERSION")));
d.kilic's avatar
d.kilic committed
            }
        try
        {
            openXml(doc, openSeq);
        }
        catch(std::domain_error &e)
        {
            // reset to settings before trying to load new file
            openXml(oldSettings);
            setProFileName(oldProFilename);

            auto errorStr = QString{"Error during reading of pet file:\n%1"}.arg(e.what());
            PCritical(this, "Could not read pet-file", errorStr);
            return;
        }
        mLastTrackerExport = mTrcFileName;

d.kilic's avatar
d.kilic committed
        updateWindowTitle();
    }
}

void Petrack::saveXml(QDomDocument &doc)
{
    QDomElement elem;

    QDomElement root = doc.createElement("PETRACK");
    root.setAttribute("VERSION", mPetrackVersion);
d.kilic's avatar
d.kilic committed
    doc.appendChild(root);

    // main settings (window size, status hight)
    elem        = doc.createElement("MAIN");
    QString seq = getFileList(mSeqFileName, mProFileName);
d.kilic's avatar
d.kilic committed

    elem.setAttribute("SRC", seq);
    elem.setAttribute("STATUS_HEIGHT", mStatusPosRealHeight->value());
    root.appendChild(elem);

    // control settings (right control widget)
    elem = doc.createElement("CONTROL");
    mControlWidget->setXml(elem);
    root.appendChild(elem);

    // Reprojection error extrinsic calib
    elem = doc.createElement("EXTR_CALIBRATION");
    mExtrCalibration.setXml(elem);
    root.appendChild(elem);

d.kilic's avatar
d.kilic committed
    // settings for stereo
    elem = doc.createElement("STEREO");
    mStereoWidget->setXml(elem);
    root.appendChild(elem);

    // settings for color marker
    elem = doc.createElement("COLOR_MARKER");
    mColorMarkerWidget->setXml(elem);
    root.appendChild(elem);

    // settings for code marker
    elem = doc.createElement("CODE_MARKER");
    mCodeMarkerWidget->setXml(elem);
    root.appendChild(elem);

    // settings for multicolor marker
    elem = doc.createElement("MULTI_COLOR_MARKER");
    mMultiColorMarkerWidget->setXml(elem);
    root.appendChild(elem);

    // settings for MoCap-Visualization
    elem = doc.createElement("MOCAP");
    mMoCapController.setXml(elem);
    root.appendChild(elem);

d.kilic's avatar
d.kilic committed
    // player settings (which frame, frame range)
    elem = doc.createElement("PLAYER");
    elem.setAttribute("FRAME", mPlayerWidget->getPos()); // == mAnimation->getCurrentFrameNum()
    elem.setAttribute("FPS", mAnimation->getFPS());
    elem.setAttribute("SOURCE_FRAME_IN", mPlayerWidget->getFrameInNum());
    elem.setAttribute("SOURCE_FRAME_OUT", mPlayerWidget->getFrameOutNum());
    elem.setAttribute("PLAYER_SPEED_FIXED", mPlayerWidget->getPlayerSpeedLimited());
d.kilic's avatar
d.kilic committed

    root.appendChild(elem);

    // view settings (zoom, rotate, alias, opengl)
    elem = doc.createElement("VIEW");
    elem.setAttribute("ANTIALIAS", mAntialiasAct->isChecked());
    elem.setAttribute("OPENGL", mOpenGLAct->isChecked());
    elem.setAttribute("SAVE_TRANSFORMED", mCropZoomViewAct->isChecked());
    elem.setAttribute(
        "TRANSFORMATION",
        QString("%1 %2 %3 %4")
            .arg(mViewWidget->getZoomLevel())
            .arg(mViewWidget->getRotateLevel())
            .arg(mView->horizontalScrollBar()->value())
            .arg(mView->verticalScrollBar()->value()));
#ifndef STEREO_DISABLED
d.kilic's avatar
d.kilic committed
    elem.setAttribute("CAMERA", mAnimation->getCamera());
#else
    elem.setAttribute("CAMERA", cameraUnset);
#endif
d.kilic's avatar
d.kilic committed
    elem.setAttribute("HIDE_CONTROLS", mHideControlsAct->isChecked());
    root.appendChild(elem);

    // auto track settings
    elem = doc.createElement("AUTO_TRACK");
    elem.setAttribute("BACK_TRACK", mAutoBackTrack);
    elem.setAttribute("OPTIMZE_COLOR", mAutoTrackOptimizeColor);
    root.appendChild(elem);

    elem = doc.createElement("MISSING_FRAMES");
    elem.setAttribute("executed", mMissingFrames.isExecuted());
    for(const auto &missingFrame : mMissingFrames.getMissingFrames())
    {
        auto frame = doc.createElement("FRAME");
        frame.setAttribute("NUM_FRAME", static_cast<int>(missingFrame.mNumber));
        frame.setAttribute("NUM_MISSING", missingFrame.mCount);
        elem.appendChild(frame);
    }
    root.appendChild(elem);
d.kilic's avatar
d.kilic committed
}

/// rueckgabewert zeigt an, ob gesichert werden konnte
d.kilic's avatar
d.kilic committed
bool Petrack::saveSameProject()
{
    return saveProject(mProFileName);
}

/**
 * Ask the user to provide a filename to save project into
 * @return true if the saving process was done successfully
 */
d.kilic's avatar
d.kilic committed
bool Petrack::saveProjectAs()
{
    // empty filename will trigger file selection box
    return saveProject("");
/**
 * Save the project to the given filename
 * @param fileName the name of the file to save the project into
 * @return true if the saving process was done successfully
 */
d.kilic's avatar
d.kilic committed
bool Petrack::saveProject(QString fileName) // default fileName=""
{
    if(fileName.isEmpty())
d.kilic's avatar
d.kilic committed
    {
        fileName = QFileDialog::getSaveFileName(
            this, tr("Select project file"), mProFileName, tr("PeTrack project file (*.pet);;All files (*.*)"));
d.kilic's avatar
d.kilic committed
    }

    // selection was cancelled
    if(fileName.isEmpty())
d.kilic's avatar
d.kilic committed
    {
d.kilic's avatar
d.kilic committed

    setProFileName(fileName);
    QDomDocument doc("PETRACK"); // eigentlich Pfad zu Beschreibungsdatei fuer Dateiaufbau
    saveXml(doc);
d.kilic's avatar
d.kilic committed

    // file output
    QByteArray       byteArray;
    QXmlStreamWriter xmlStream(&byteArray);
    xmlStream.setAutoFormatting(true);
    xmlStream.setAutoFormattingIndent(4);
d.kilic's avatar
d.kilic committed

    xmlStream.writeStartDocument();
    xmlStream.writeDTD("<!DOCTYPE PETRACK>");
d.kilic's avatar
d.kilic committed

    QDomElement element = doc.documentElement();
    writeXmlElement(xmlStream, element);
d.kilic's avatar
d.kilic committed

    xmlStream.writeEndDocument();
d.kilic's avatar
d.kilic committed

    QFile file(fileName);
    if(!file.open(QFile::WriteOnly | QFile::Truncate | QFile::Text))
        PCritical(this, tr("PeTrack"), tr("Cannot save %1:\n%2.").arg(fileName, file.errorString()));
        file.close();
d.kilic's avatar
d.kilic committed
        return false;
    file.write(byteArray);
    file.close(); // also flushes the file

    statusBar()->showMessage(tr("Saved project to %1.").arg(fileName), 5000);
    SPDLOG_INFO("save project to {}", fileName);

    updateWindowTitle();
    return true;
d.kilic's avatar
d.kilic committed
}

void Petrack::writeXmlElement(QXmlStreamWriter &xmlStream, QDomElement element)
d.kilic's avatar
d.kilic committed
{
    xmlStream.writeStartElement(element.tagName());

    QVector<QString>       attribute_names;
d.kilic's avatar
d.kilic committed
    const QDomNamedNodeMap attributes = element.attributes();
    for(int i = 0; i < attributes.size(); ++i)
    {
d.kilic's avatar
d.kilic committed
        attribute_names.push_back(attributes.item(i).toAttr().name());
    }

    // TODO: check if sorting of elements fits our needs
d.kilic's avatar
d.kilic committed
    std::stable_sort(attribute_names.begin(), attribute_names.end()); // for a canonical XML

    // Wants this macro instead of range-based for loop
    foreach(QString name, attribute_names)
    {
d.kilic's avatar
d.kilic committed
        QDomAttr attr = element.attributeNode(name);
        xmlStream.writeAttribute(attr.name(), attr.value());
    }

    // order of child nodes is defined at creation
    if(element.hasChildNodes())
    {
d.kilic's avatar
d.kilic committed
        const QDomNodeList children = element.childNodes();
        for(int i = 0; i < children.size(); ++i)
        {
d.kilic's avatar
d.kilic committed
            writeXmlElement(xmlStream, children.at(i).toElement());
        }
    }

    xmlStream.writeEndElement();
}