[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/FastTrackOrg/FastTrack/master/src/interactive.cpp [Back]  [Original]

/*
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() 

Web Proxy Viewer  |  New URL  |  Original Page