/*
This file is part of Fast Track.
FastTrack 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.
FastTrack 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 FastTrack. If not, see .
*/
#include "interactive.h"
#include "ui_interactive.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/**
* @class Interactive
*
* @brief The Interactive widget provides an environment to use the tracking widget in an interactive environment.
*
* @author Benjamin Gallois
*
* @version $Revision: 480 $
*
* Contact: benjamin.gallois@fasttrack.sh
*
*/
/**
* @brief Constructs the interactive object derived from a QMainWindow object.
*/
Interactive::Interactive(QWidget *parent) : QMainWindow(parent),
ui(new Ui::Interactive),
video(new VideoReader()),
videoStatus(false) {
ui->setupUi(this);
controls = new RuntimeControls;
controls->progressBar = ui->trackingProgressBar;
parameterValues = {{QStringLiteral("maxArea"), 0}, {QStringLiteral("minArea"), 0}, {QStringLiteral("spot"), 0}, {QStringLiteral("normDist"), 100.0}, {QStringLiteral("normAngle"), 180.0}, {QStringLiteral("maxDist"), 0}, {QStringLiteral("maxTime"), 10}, {QStringLiteral("normArea"), 0.0}, {QStringLiteral("normPerim"), 0.0}, {QStringLiteral("thresh"), 127}, {QStringLiteral("nBack"), 1}, {QStringLiteral("methBack"), 0}, {QStringLiteral("regBack"), 0}, {QStringLiteral("reg"), 0}, {QStringLiteral("morph"), 8}, {QStringLiteral("morphSize"), 0}, {QStringLiteral("morphType"), 2}, {QStringLiteral("lightBack"), 0}, {QStringLiteral("xTop"), 0}, {QStringLiteral("yTop"), 0}, {QStringLiteral("xBottom"), 0}, {QStringLiteral("yBottom"), 0}, {QStringLiteral("startImage"), 0}, {QStringLiteral("stopImage"), 0}, {QStringLiteral("frame"), 0}, {QStringLiteral("displayMode"), 0}};
ui->interactiveTab->setFocusPolicy(Qt::NoFocus);
// MetaType
qRegisterMetaType("QHash");
connect(ui->interactiveTab, &QTabWidget::currentChanged, this, [this](int index) {
emit activeViewChanged(index == 1 && replayVisible);
});
connect(this, &Interactive::message, this, [](const QString &msg) {
QMessageBox msgBox;
msgBox.setIcon(QMessageBox::Critical);
msgBox.setText(tr("Error"));
msgBox.setInformativeText(msg);
msgBox.exec();
});
replay = new Replay(this, video);
connect(ui->interactiveTab, &QTabWidget::tabCloseRequested, this, [this](int index) {
if (index != 0) {
setReplayVisible(false);
}
});
// Events filter to select ROI
ui->display->installEventFilter(this);
isBackground = false;
// Sets a color map
colorMap.reserve(1000000);
int a, b, c;
srand((unsigned int)time(NULL));
for (int j = 0; j < 1000000; ++j) {
a = rand() % 255;
b = rand() % 255;
c = rand() % 255;
colorMap.push_back(Point3i(a, b, c));
}
// Sets the object counter on top of the display
counterLabel = new QLabel(ui->display); // TODO put as QGraphicsItem
counterLabel->move(20, 20);
}
void Interactive::setReplayVisible(bool visible) {
if (replayVisible == visible) {
return;
}
replayVisible = visible;
if (visible) {
ui->interactiveTab->addTab(replay, tr("Replay"));
ui->interactiveTab->setCurrentIndex(1);
}
else {
while (ui->interactiveTab->count() > 1) {
ui->interactiveTab->removeTab(1);
}
}
emit replayVisibleChanged(visible);
}
QHash Interactive::parameterState() const {
return parameterValues;
}
void Interactive::setParameterState(const QHash &state) {
if (state.isEmpty()) {
return;
}
QScopedValueRollback applying(applyingParameterState, true);
for (auto it = state.cbegin(); it != state.cend(); ++it) {
parameterValues.insert(it.key(), it.value());
}
display(parameterInt(QStringLiteral("frame")));
}
Replay *Interactive::replayWidget() const {
return replay;
}
void Interactive::preview() {
previewTracking();
}
void Interactive::startTracking() {
if (QMessageBox::question(this,
tr("Confirmation"),
tr("You are going to start a full tracking analysis. That can take some time, are you sure?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
track();
}
}
void Interactive::computeWorkspaceBackground() {
computeBackground();
}
void Interactive::selectWorkspaceBackground() {
selectBackground();
}
void Interactive::cropWorkspace() {
crop();
}
void Interactive::resetWorkspaceCrop() {
reset();
}
void Interactive::levelWorkspaceParameters() {
level();
}
bool Interactive::canPreview() const {
return trackingAvailable;
}
bool Interactive::canTrack() const {
return trackingAvailable;
}
bool Interactive::isReplayVisible() const {
return replayVisible;
}
bool Interactive::isReplayActive() const {
return replayVisible && ui->interactiveTab->currentIndex() == 1;
}
bool Interactive::isTimelineEnabled() const {
return timelineEnabled;
}
int Interactive::frameCount() const {
return video->isOpened() ? static_cast(video->getImageCount()) : 0;
}
QList Interactive::informationValues() const {
return informationValuesState;
}
QString Interactive::trackingStatusText() const {
return trackingStatusState;
}
void Interactive::setTrackingAvailable(bool available) {
trackingAvailable = available;
emit trackingAvailabilityChanged();
}
int Interactive::parameterInt(const QString &name) const {
return parameterValues.value(name).toInt();
}
double Interactive::parameterDouble(const QString &name) const {
return parameterValues.value(name).toDouble();
}
void Interactive::setParameter(const QString &name, const QVariant &value) {
if (parameterValues.value(name) == value) {
return;
}
parameterValues.insert(name, value);
notifyParameterStateChanged();
}
void Interactive::notifyParameterStateChanged() {
if (!applyingParameterState) {
emit parameterStateChanged(parameterState());
}
}
void Interactive::updateProgressEstimate(int value) {
controls->progressBar->setValue(value);
const int completed = value - controls->progressBar->minimum() + 1;
const int total = controls->progressBar->maximum() - controls->progressBar->minimum() + 1;
if (completed setFormat(tr("%p% - %1 remaining").arg(remaining));
}
/**
* @brief Asks the path to a folder where an image sequence is stored. Setups the ui and resets the class attributs for a new analysis. Triggered when the open button from the menu bar is clicked.
*/
void Interactive::openFolder(QString path) {
// Resets the class members
setReplayVisible(false);
videoStatus = false;
isBackground = false;
memoryDir.clear();
backgroundPath.clear();
background.release();
// Resets the ui
ui->display->clear();
video->release();
replay->clear();
parameterValues.insert(QStringLiteral("displayMode"), 0);
if (path.isEmpty()) {
dir = QFileDialog::getOpenFileName(this, tr("Open File"), memoryDir);
}
else {
dir = path;
}
QApplication::setOverrideCursor(Qt::WaitCursor);
if (dir.length()) {
// Finds the image format
// Setups the class member
try {
memoryDir = dir;
video->open(dir.toStdString());
setTrackingAvailable(false);
trackingStatusState = tr("Compute the background to continue");
parameterValues.insert(QStringLiteral("nBack"), static_cast(video->getImageCount()));
parameterValues.insert(QStringLiteral("startImage"), 0);
parameterValues.insert(QStringLiteral("stopImage"), static_cast(video->getImageCount()));
Mat frame;
video->getImage(0, frame);
cvtColor(frame, frame, COLOR_GRAY2RGB);
originalImageSize.setWidth(frame.cols);
originalImageSize.setHeight(frame.rows);
cropedImageSize.setWidth(originalImageSize.width());
cropedImageSize.setHeight(originalImageSize.height());
informationValuesState[0] = dir;
informationValuesState[1] = QString::number(video->getImageCount());
informationValuesState[2] = QString::number(frame.cols);
informationValuesState[3] = QString::number(frame.rows);
isBackground = false;
reset();
// Load replay
replay->loadReplay(dir);
if (!replay->trackingData->isEmpty) {
setReplayVisible(true);
}
// Load parameters
QFileInfo savingInfo(dir);
QString savingFilename = savingInfo.baseName();
QString cfgFile = savingInfo.absolutePath();
if (video->isSequence()) {
cfgFile.append(QStringLiteral("/Tracking_Result") + QDir::separator() + "cfg.toml");
}
else {
cfgFile.append(QStringLiteral("/Tracking_Result_") + savingFilename + QDir::separator() + "cfg.toml");
}
if (QFileInfo::exists(cfgFile)) {
loadParameters(cfgFile);
const QString savedBackground = QFileInfo(cfgFile).absoluteDir().filePath(QStringLiteral("background.pgm"));
imread(savedBackground.toStdString(), IMREAD_GRAYSCALE | IMREAD_ANYDEPTH).copyTo(background);
if (!background.empty() && background.cols == originalImageSize.width() && background.rows == originalImageSize.height()) {
backgroundPath = savedBackground;
isBackground = true;
setTrackingAvailable(true);
trackingStatusState.clear();
}
}
if (video->isOpened()) {
videoStatus = true;
emit inputOpened(QFileInfo(dir).absoluteFilePath());
}
display(0);
ui->display->fitToView();
crop();
}
// If an error occurs during the opening, resets the information table and warns the user
catch (exception &e) {
informationValuesState = {QString(), QStringLiteral("0"), QStringLiteral("0"), QStringLiteral("0"), QStringLiteral("0")};
qWarning() setAttribute(Qt::WA_DeleteOnClose);
connect(warning, &QMessageBox::finished, this, [this, index, scale](int result) {
contourWarningOpen = false;
largeContourDisplayDecision = (result == QMessageBox::Yes) ? 1 : 0;
if (largeContourDisplayDecision == 1) {
display(index, scale);
}
});
warning->open();
return;
}
}
else {
// Ask again after the contour count has dropped below the warning
// threshold and subsequently crosses it again.
largeContourDisplayDecision = -1;
}
vector displayContours;
vector rejectedContours;
displayContours.reserve(contours.size());
rejectedContours.reserve(contours.size());
for (auto const &a : contours) {
double size = contourArea(a);
if (size > min && size < max) {
displayContours.push_back(a);
}
else {
rejectedContours.push_back(a);
}
}
cvtColor(frame, frame, COLOR_GRAY2RGB);
drawContours(frame, displayContours, -1, Scalar(0, 255, 0), FILLED, 8);
drawContours(frame, rejectedContours, -1, Scalar(255, 0, 0), FILLED, 8);
counterLabel->setText("Objects detected: " + QString::number(displayContours.size()));
counterLabel->adjustSize();
}
else {
cvtColor(frame, frame, COLOR_GRAY2RGB);
}
// Draws the scale
if (scale != 0) {
line(frame, Point(20, 20), Point(20 + scale, 20), Scalar(255, 0, 0), 2);
}
// Crops the image
if ((roi.width != 0 || roi.height != 0) && (roi.width != originalImageSize.width() || roi.height != originalImageSize.height())) {
frame = frame(roi);
}
display(frame);
}
catch (const std::exception &e) {
qWarning() result().clone(); // Clone needed for Windows otherwise no initial display and ui buggued afterward, why if UMat is a smartpointer?
if (!background.empty()) {
isBackground = true;
// Automatic background type selection based on the image mean
int meanValue = int(mean(background)[0]);
parameterValues.insert(QStringLiteral("lightBack"), meanValue > 128 ? 0 : 1);
ui->interactiveTab->setCurrentIndex(0);
setTrackingAvailable(true);
trackingStatusState.clear();
display(background, QImage::Format_Grayscale8);
}
else {
isBackground = false;
parameterValues.insert(QStringLiteral("displayMode"), 0);
setTrackingAvailable(false);
}
this->setEnabled(true);
QApplication::restoreOverrideCursor();
watcher->deleteLater();
});
// Compute background process
QFuture future = QtConcurrent::run([=]() {
UMat background;
try {
VideoReader backgroundVideo(videoPath);
if (!backgroundVideo.open()) {
throw std::runtime_error("Background computation error: the video can not be opened.");
}
background = Tracking::backgroundExtraction(backgroundVideo, nBack, method, registrationMethod);
}
catch (const std::runtime_error &e) {
qWarning() progressBar->setRange(startImage, startImage + stopImage - 1);
controls->progressBar->setValue(0);
controls->progressBar->setFormat(tr("%p% - estimating..."));
trackingElapsed.start();
setTrackingAvailable(false);
setReplayVisible(false);
replay->clear(); // Avoid mixing 2 subsequent analysy
QThread *thread = new QThread;
Tracking *tracking = new Tracking(memoryDir.toStdString(), background, startImage, startImage + stopImage);
tracking->moveToThread(thread);
connect(thread, &QThread::started, tracking, &Tracking::startProcess);
connect(tracking, &Tracking::progress, this, &Interactive::updateProgressEstimate);
connect(tracking, &Tracking::statistic, this, [this, stopImage](int time) {
informationValuesState[4] = QString::number(double(stopImage * 1000) / double(time));
});
connect(tracking, &Tracking::finished, this, [this]() {
controls->progressBar->setValue(controls->progressBar->maximum());
controls->progressBar->setFormat(tr("Done"));
timelineEnabled = true;
emit timelineEnabledChanged(timelineEnabled);
setTrackingAvailable(true);
replay->loadReplay(dir);
setReplayVisible(true);
});
connect(tracking, &Tracking::forceFinished, this, [this](const QString &errorMessage) {
controls->progressBar->setFormat(tr("Stopped"));
timelineEnabled = true;
emit timelineEnabledChanged(timelineEnabled);
setTrackingAvailable(true);
replay->loadReplay(dir);
setReplayVisible(true);
emit message(errorMessage);
});
connect(tracking, &Tracking::forceFinished, thread, &QThread::quit);
connect(tracking, &Tracking::forceFinished, tracking, &Tracking::deleteLater);
connect(tracking, &Tracking::finished, tracking, &Tracking::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
getParameters();
tracking->updatingParameters(parameters);
thread->start();
timelineEnabled = false;
emit timelineEnabledChanged(timelineEnabled);
}
}
/**
* @brief Does a full tracking analysis.
*/
void Interactive::track() {
if (videoStatus) {
controls->progressBar->setRange(0, static_cast(video->getImageCount()) - 1);
controls->progressBar->setValue(0);
controls->progressBar->setFormat(tr("%p% - estimating..."));
trackingElapsed.start();
setTrackingAvailable(false);
setReplayVisible(false);
replay->clear(); // Avoid mixing 2 subsequent analysy
QThread *thread = new QThread;
Tracking *tracking = new Tracking(memoryDir.toStdString(), background);
QSharedPointer logMap(new QHash);
logMap->insert(QStringLiteral("date"), QDateTime::currentDateTime().toString());
logMap->insert(QStringLiteral("path"), dir);
tracking->moveToThread(thread);
connect(thread, &QThread::started, tracking, &Tracking::startProcess);
connect(tracking, &Tracking::progress, this, &Interactive::updateProgressEstimate);
connect(tracking, &Tracking::statistic, this, [this, logMap](int time) {
informationValuesState[4] = QString::number(double(video->getImageCount() * 1000) / double(time));
logMap->insert(QStringLiteral("time"), QString::number(time));
});
connect(tracking, &Tracking::forceFinished, this, [this, logMap](const QString &errorMessage) {
controls->progressBar->setFormat(tr("Stopped"));
timelineEnabled = true;
emit timelineEnabledChanged(timelineEnabled);
setTrackingAvailable(true);
replay->loadReplay(dir);
setReplayVisible(true);
logMap->insert(QStringLiteral("status"), errorMessage);
emit log(*logMap);
emit status(tr("Tracking failed"));
emit message(errorMessage);
});
connect(tracking, &Tracking::finished, thread, &QThread::quit);
connect(tracking, &Tracking::finished, this, [this, logMap]() {
controls->progressBar->setValue(controls->progressBar->maximum());
controls->progressBar->setFormat(tr("Done"));
timelineEnabled = true;
emit timelineEnabledChanged(timelineEnabled);
setTrackingAvailable(true);
replay->loadReplay(dir);
setReplayVisible(true);
logMap->insert(QStringLiteral("status"), QStringLiteral("Done"));
emit log(*logMap);
emit status(tr("Tracking succeeded"));
});
connect(tracking, &Tracking::forceFinished, thread, &QThread::quit);
connect(tracking, &Tracking::forceFinished, tracking, &Tracking::deleteLater);
connect(tracking, &Tracking::finished, tracking, &Tracking::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
getParameters();
tracking->updatingParameters(parameters);
thread->start();
timelineEnabled = false;
emit timelineEnabledChanged(timelineEnabled);
}
}
/**
* @brief Manages all the mouse inputs in the display.
* @param[in] target Widget to apply the filter.
* @param[in] event Describes the mouse event.
*/
bool Interactive::eventFilter(QObject *target, QEvent *event) {
// Mouse event for the display
if (target == ui->display) {
// Set the first point for the ROI at user click
if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouseEvent = dynamic_cast(event);
if (mouseEvent->buttons() == Qt::LeftButton) {
clicks.first = ui->display->mapToScene(mouseEvent->pos());
}
}
// Sets the second point and draw the roi
if (event->type() == QEvent::MouseMove) {
QMouseEvent *moveEvent = dynamic_cast(event);
if (moveEvent->buttons() == Qt::LeftButton) {
clicks.second = ui->display->mapToScene(moveEvent->pos());
// Draws the ROI with
QRect roiRect = QRect(clicks.first.x(), clicks.first.y(), clicks.second.x() - clicks.first.x(), clicks.second.y() - clicks.first.y());
ui->display->setRectangle(roiRect);
// Updates ui value
// Find the rectangle top corner
int xTop, yTop, xBottom, yBottom;
if (clicks.first.y() < clicks.second.y()) {
xTop = clicks.first.x();
yTop = clicks.first.y();
xBottom = clicks.second.x();
yBottom = clicks.second.y();
}
else {
xBottom = clicks.first.x();
yBottom = clicks.first.y();
xTop = clicks.second.x();
yTop = clicks.second.y();
}
// Find the left corner of the rectangle
int width = xBottom - xTop;
if (width < 0) {
xTop += width;
xBottom -= width;
}
// Converts clicks from display widget frame of reference to original image frame of reference
parameterValues.insert(QStringLiteral("xTop"), xTop + roi.tl().x);
parameterValues.insert(QStringLiteral("yTop"), yTop + roi.tl().y);
parameterValues.insert(QStringLiteral("xBottom"), xBottom + roi.tl().x);
parameterValues.insert(QStringLiteral("yBottom"), yBottom + roi.tl().y);
notifyParameterStateChanged();
}
}
}
return QWidget::eventFilter(target, event);
}
/**
* @brief Crops the image from a rectangle drawed by the user with the mouse on the display. Triggered when the QPushButton ui->crop is clicked.
*/
void Interactive::crop() {
int xTop = parameterInt(QStringLiteral("xTop"));
int yTop = parameterInt(QStringLiteral("yTop"));
int xBottom = parameterInt(QStringLiteral("xBottom"));
int yBottom = parameterInt(QStringLiteral("yBottom"));
// Checks for wrong values
int width = xBottom - xTop;
int height = yBottom - yTop;
if (xTop < roi.tl().x) xTop = roi.tl().x;
if (yTop < roi.tl().y) yTop = roi.tl().y;
if (width > cropedImageSize.width() - xTop + roi.tl().x) width = cropedImageSize.width() - xTop + roi.tl().x;
if (height > cropedImageSize.height() - yTop + roi.tl().y) height = cropedImageSize.height() - yTop + roi.tl().y;
roi = Rect(xTop, yTop, width, height);
cropedImageSize.setWidth(roi.width);
cropedImageSize.setHeight(roi.height);
informationValuesState[2] = QString::number(roi.width);
informationValuesState[3] = QString::number(roi.height);
display(parameterInt(QStringLiteral("frame")));
parameterValues.insert(QStringLiteral("xTop"), roi.tl().x);
parameterValues.insert(QStringLiteral("yTop"), roi.tl().y);
parameterValues.insert(QStringLiteral("xBottom"), roi.br().x);
parameterValues.insert(QStringLiteral("yBottom"), roi.br().y);
notifyParameterStateChanged();
ui->display->setRectangle(QRect());
}
/**
* @brief Resets the region of interest. Triggered by the reset button.
*/
void Interactive::reset() {
cropedImageSize.setWidth(originalImageSize.width());
cropedImageSize.setHeight(originalImageSize.height());
parameterValues.insert(QStringLiteral("xTop"), 0);
parameterValues.insert(QStringLiteral("yTop"), 0);
parameterValues.insert(QStringLiteral("xBottom"), originalImageSize.width());
parameterValues.insert(QStringLiteral("yBottom"), originalImageSize.height());
informationValuesState[2] = QString::number(originalImageSize.width());
informationValuesState[3] = QString::number(originalImageSize.height());
roi = Rect(0, 0, 0, 0);
ui->display->setRectangle(QRect());
display(parameterInt(QStringLiteral("frame")));
notifyParameterStateChanged();
}
/**
* @brief Destructors.
*/
Interactive::~Interactive() {
ui->interactiveTab->setCurrentIndex(0);
delete video;
delete controls;
delete ui;
}
/**
* @brief Level the parameters.
*/
void Interactive::level() {
if (videoStatus) {
try {
QThread *thread = new QThread;
getParameters();
AutoLevel *autolevel = new AutoLevel(memoryDir.toStdString(), background, parameters);
autolevel->moveToThread(thread);
connect(thread, &QThread::started, autolevel, &AutoLevel::level);
connect(autolevel, &AutoLevel::forceFinished, this, [this](const QString &errorMessage) {
this->setEnabled(true);
emit message(errorMessage);
});
connect(autolevel, &AutoLevel::finished, this, [this]() {
this->setEnabled(true);
});
connect(autolevel, &AutoLevel::levelParametersChanged, this, [this](const QHash &levelParameters) {
parameterValues.insert(QStringLiteral("normAngle"), levelParameters.value(QStringLiteral("normAngle")));
parameterValues.insert(QStringLiteral("normDist"), levelParameters.value(QStringLiteral("normDist")));
parameterValues.insert(QStringLiteral("normArea"), levelParameters.value(QStringLiteral("normArea")));
parameterValues.insert(QStringLiteral("normPerim"), levelParameters.value(QStringLiteral("normPerim")));
notifyParameterStateChanged();
QApplication::restoreOverrideCursor();
});
connect(autolevel, &AutoLevel::finished, thread, &QThread::quit);
connect(autolevel, &AutoLevel::finished, autolevel, &AutoLevel::deleteLater);
connect(autolevel, &AutoLevel::forceFinished, thread, &QThread::quit);
connect(autolevel, &AutoLevel::forceFinished, autolevel, &AutoLevel::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
QApplication::setOverrideCursor(Qt::WaitCursor);
thread->start();
this->setEnabled(false);
}
catch (const std::exception &e) {
qWarning()