diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..13ad5df
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+plates.exe
+*.app
+obj
+*.pro.user*
+*~
+*.autosave
+*.a
+*.core
+*.moc
+*.o
+*.obj
+*.orig
+*.rej
+*_pch.h.cpp
+*_resource.rc
+*.qm
+.#*
+*.*#
+core
+!core/
+tags
+.DS_Store
+.directory
+*.debug
+Makefile*
+*.prl
+*.app
+moc_*.cpp
+ui_*.h
+qrc_*.cpp
+Thumbs.db
+*.res
+/.qmake.cache
+/.qmake.stash
+build/
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..3208e8b
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,59 @@
+cmake_minimum_required(VERSION 2.8.11)
+
+project(openalpr_tagger)
+
+SET(VERSION_MAJOR 1)
+SET(VERSION_MINOR 0)
+SET(VERSION_PATCH 0)
+
+SET (VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
+add_definitions(-DVERSION_MAJOR=${VERSION_MAJOR})
+add_definitions(-DVERSION_MINOR=${VERSION_MINOR})
+add_definitions(-DVERSION_PATCH=${VERSION_PATCH})
+add_definitions(-DVERSION_STRING="${VERSION}")
+
+
+add_definitions(-DTARGET="openalpr_tagger")
+add_definitions(-DTARGET_STRING="openalpr_tagger")
+add_definitions(-DTARGET_UPPER_STRING="OPENALPR_TAGGER")
+add_definitions(-DTARGET_HUMAN_STRING="openalpr_tagger")
+
+ADD_DEFINITIONS(
+ -std=c++11
+)
+
+
+# Find the QtWidgets library
+find_package(Qt5Widgets)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTOUIC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+# Tell CMake to create the helloworld executable
+add_executable(openalpr_tagger
+about.cpp
+directoryloader.cpp
+dot.cpp
+imagefile.cpp
+imageview.cpp
+imageviewerbase.cpp
+imageviewerobserver.cpp
+imageviewerplateselector.cpp
+main.cpp
+mainwindow.cpp
+options.cpp
+platefile.cpp
+plateselector.cpp
+polygons.cpp
+selection.cpp
+settings.cpp
+squeezedlabel.cpp
+threadedimageloader.cpp
+utils.cpp
+qprogressindicator/QProgressIndicator.cpp
+)
+
+include_directories(${CMAKE_SOURCE_DIR}/qprogressindicator)
+# Use the Widgets module from Qt 5.
+target_link_libraries(openalpr_tagger Qt5::Widgets)
diff --git a/Info.plist b/Info.plist
new file mode 100644
index 0000000..e1401d9
--- /dev/null
+++ b/Info.plist
@@ -0,0 +1,34 @@
+
+
+
+
+ NSPrincipalClass
+ NSApplication
+ NSHumanReadableCopyright
+ (C) 2017, Dmitry Baryshev
+ CFBundleDevelopmentRegion
+ en
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleIconFile
+ plates.icns
+ CFBundlePackageType
+ APPL
+ CFBundleGetInfoString
+ OpenALPR Training Utility
+ CFBundleSignature
+ ????
+ CFBundleExecutable
+ plates
+ CFBundleIdentifier
+ net.plates
+ CFBundleName
+ OpenALPR Training Utility
+ CFBundleDisplayName
+ OpenALPR Training Utility
+ CFBundleShortVersionString
+ 1.0.0
+ CFBundleVersion
+ 1.0.0
+
+
diff --git a/about.cpp b/about.cpp
new file mode 100644
index 0000000..2c69297
--- /dev/null
+++ b/about.cpp
@@ -0,0 +1,32 @@
+#include
+
+#include "about.h"
+#include "ui_about.h"
+
+About::About(QWidget *parent)
+ : QDialog(parent)
+ , ui(new Ui::About)
+{
+ ui->setupUi(this);
+
+ ui->labelProduct->setText(TARGET_HUMAN_STRING);
+ ui->labelVersion->setText(tr("Version %1").arg(VERSION_STRING));
+ ui->labelIcon->setPixmap(QIcon(":/images/" TARGET_STRING ".ico").pixmap(48, 48));
+
+ // %1 will be replaced with a product name
+ ui->labelWarranty->setText(tr("%1 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.")
+ .arg(TARGET_HUMAN_STRING));
+
+#ifdef Q_OS_MAC
+ ui->pushOK->hide();
+#else
+ setWindowTitle(tr("About"));
+#endif
+
+ adjustSize();
+}
+
+About::~About()
+{
+ delete ui;
+}
diff --git a/about.h b/about.h
new file mode 100644
index 0000000..632cc91
--- /dev/null
+++ b/about.h
@@ -0,0 +1,23 @@
+#ifndef ABOUT_H
+#define ABOUT_H
+
+#include
+
+namespace Ui
+{
+ class About;
+}
+
+class About : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit About(QWidget *parent = 0);
+ ~About();
+
+private:
+ Ui::About *ui;
+};
+
+#endif // ABOUT_H
diff --git a/about.ui b/about.ui
new file mode 100644
index 0000000..710b03a
--- /dev/null
+++ b/about.ui
@@ -0,0 +1,193 @@
+
+
+ About
+
+
+
+ 0
+ 0
+ 280
+ 124
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 0
+ 20
+
+
+
+
+ -
+
+
+ OK
+
+
+ true
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 0
+ 20
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 0
+ 20
+
+
+
+
+ -
+
+
+
+
+
+ Qt::AlignJustify|Qt::AlignVCenter
+
+
+ true
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 0
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 0
+ 20
+
+
+
+
+ -
+
+
+ 8
+
+
-
+
+
+
+ 75
+ true
+
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 0
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 6
+
+
+
+
+
+
+
+
+
+ pushOK
+ clicked()
+ About
+ accept()
+
+
+ 175
+ 131
+
+
+ 423
+ 230
+
+
+
+
+
+ slotShowIPs()
+
+
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..07373e6
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+plates (1.0.0-1) unstable; urgency=medium
+
+ * Initial release.
+
+ -- Dmitry Baryshev Fri, 07 Apr 2017 14:51:50 +0300
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..894e9d9
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,18 @@
+Source: plates
+Section: education
+Priority: extra
+Maintainer: Dmitry Baryshev
+XSBC-Original-Maintainer: Dmitry Baryshev
+Uploaders: Dmitry Baryshev
+Build-Depends: debhelper (>= 9), qtbase5-dev, qttools5-dev-tools, imagemagick
+Standards-Version: 3.9.4
+Homepage: https://github.com/openalpr/openalpr
+#Vcs-Git:
+#Vcs-Browser:
+
+Package: plates
+Architecture: any
+Multi-Arch: foreign
+Depends: ${misc:Depends}, ${shlibs:Depends}
+Description: Plates is an OpenALPR training utility
+ to manually find and mark license plates on photos
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..a0189d0
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,35 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: plates
+Source: https://github.com/openalpr/openalpr
+
+Files: *
+Copyright: 2017 Dmitry Baryshev
+License:
+
+Files: debian/*
+Copyright: 2016 Dmitry Baryshev
+License: LGPL-2.1
+
+License: GPL-3
+ GNU General Public License Usage
+ .
+ Alternatively, this file may be used under the terms of the GNU
+ General Public License version 3.0 as published by the Free Software
+ Foundation and appearing in the file LICENSE.GPL included in the
+ packaging of this file. Please review the following information to
+ ensure the GNU General Public License version 3.0 requirements will be
+ met: http://www.gnu.org/copyleft/gpl.html.
+ .
+ On Debian systems, the complete text of the license can be found in
+ `/usr/share/common-licenses/GPL-3`.
+
+License: LGPL-2.1
+ This file may be used under the terms of the GNU Lesser
+ General Public License version 2.1 as published by the Free Software
+ Foundation and appearing in the file LICENSE.LGPL included in the
+ packaging of this file. Please review the following information to
+ ensure the GNU Lesser General Public License version 2.1 requirements
+ will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+ .
+ On Debian systems, the complete text of the LGPL-2.1 license can be found in
+ `/usr/share/common-licenses/LGPL-2.1`,
diff --git a/debian/extract-icons b/debian/extract-icons
new file mode 100755
index 0000000..3fd34fc
--- /dev/null
+++ b/debian/extract-icons
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+
+if ! which identify >/dev/null 2>&1; then
+ echo "Fatal: 'identify' is not installed" >&2
+ exit 1
+fi
+
+if ! which convert >/dev/null 2>&1; then
+ echo "Fatal: 'convert' is not installed" >&2
+ exit 1
+fi
+
+ico="$1"
+template="$2"
+
+######################################
+
+identify "$ico" | while read str; do
+ name="$(echo "$str" | awk '{ print $1 }')"
+ res="$(echo "$str" | awk '{ print $3 }')"
+
+ if [ -z "$name" -o -z "$res" ]; then
+ echo "Cannot extract icons information" >&2
+ exit 1
+ fi
+
+ convert "$name" "$template-$res.png"
+
+ echo "$res"
+done
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..1302d86
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,26 @@
+#!/usr/bin/make -f
+
+DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH)
+
+# Qt5
+export QT_SELECT := qt5
+
+%:
+ dh $@ --parallel
+
+override_dh_auto_configure:
+ qmake CONFIG+=release
+
+override_dh_auto_install:
+ install -D -m 755 plates $(CURDIR)/debian/plates/usr/bin/plates
+ strip $(CURDIR)/debian/plates/usr/bin/plates
+ # extract and install icons
+ $(CURDIR)/debian/extract-icons images/plates.ico plates | while read res; do \
+ install -D -m 644 plates-$${res}.png $(CURDIR)/debian/plates/usr/share/icons/hicolor/$${res}/apps/plates.png; \
+ rm -f plates-$${res}.png; \
+ done
+# # install icons
+# for i in 16 32 48 64 128 256; do install -D -m 644 plates-$${i}x$${i}.png $(CURDIR)/debian/plates/usr/share/icons/hicolor/$${i}x$${i}/apps/plates.png; done
+# for i in 16 32 48 64 128 256; do rm -f plates-$${i}x$${i}.png; done
+ install -D -m 644 man/plates.1 $(CURDIR)/debian/plates/usr/share/man/man1/plates.1
+ install -D -m 644 plates.desktop $(CURDIR)/debian/plates/usr/share/applications/plates.desktop
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/directoryloader.cpp b/directoryloader.cpp
new file mode 100644
index 0000000..96346cf
--- /dev/null
+++ b/directoryloader.cpp
@@ -0,0 +1,63 @@
+#include
+#include
+
+#include "directoryloader.h"
+#include "utils.h"
+#include "ui_directoryloader.h"
+
+DirectoryLoader::DirectoryLoader(const QString &dirPath, QWidget *parent)
+ : QDialog(parent)
+ , ui(new Ui::DirectoryLoader)
+{
+ ui->setupUi(this);
+ setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
+
+ // directory iterator
+ m_iterator = new QDirIterator(dirPath,
+ Utils::imageMatchingWildcard(),
+ QDir::Files | QDir::Readable);
+
+ ui->progress->startAnimation();
+
+ // start iterating immediately
+ QTimer::singleShot(0, this, SLOT(slotLoadNext()));
+}
+
+DirectoryLoader::~DirectoryLoader()
+{
+ delete m_iterator;
+ delete ui;
+}
+
+void DirectoryLoader::keyPressEvent(QKeyEvent *e)
+{
+ if(e->key() == Qt::Key_Escape)
+ {
+ e->accept();
+ return;
+ }
+
+ QDialog::keyPressEvent(e);
+}
+
+void DirectoryLoader::slotLoadNext()
+{
+ if(m_iterator->hasNext())
+ {
+ m_iterator->next();
+
+ // append file & update status label
+ m_imageFiles.append(m_iterator->fileInfo());
+ ui->labelFound->setNum(m_imageFiles.size());
+
+ // next iteration
+ QTimer::singleShot(0, this, SLOT(slotLoadNext()));
+ }
+ else
+ {
+ if(m_imageFiles.isEmpty())
+ reject();
+ else
+ accept();
+ }
+}
diff --git a/directoryloader.h b/directoryloader.h
new file mode 100644
index 0000000..2f783a2
--- /dev/null
+++ b/directoryloader.h
@@ -0,0 +1,51 @@
+#ifndef DIRECTORYLOADER_H
+#define DIRECTORYLOADER_H
+
+#include
+#include
+
+#include "imagefile.h"
+
+class QDirIterator;
+
+namespace Ui
+{
+ class DirectoryLoader;
+}
+
+/*
+ * Directory loader with a spinning progress indicator. Calls accept()
+ * when some files has been found
+ */
+class DirectoryLoader : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit DirectoryLoader(const QString &dirPath, QWidget *parent = 0);
+ ~DirectoryLoader();
+
+ /*
+ * Found image files
+ */
+ ImageFileList imageFiles() const;
+
+protected:
+ virtual void keyPressEvent(QKeyEvent *e) override;
+
+private slots:
+ void slotLoadNext();
+
+private:
+ Ui::DirectoryLoader *ui;
+ QDirIterator *m_iterator;
+ ImageFileList m_imageFiles;
+};
+
+inline
+ImageFileList DirectoryLoader::imageFiles() const
+{
+ return m_imageFiles;
+}
+
+#endif // DIRECTORYLOADER_H
diff --git a/directoryloader.ui b/directoryloader.ui
new file mode 100644
index 0000000..8ebefe2
--- /dev/null
+++ b/directoryloader.ui
@@ -0,0 +1,80 @@
+
+
+ DirectoryLoader
+
+
+
+ 0
+ 0
+ 167
+ 40
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ QFrame::Box
+
+
+
-
+
+
+ Found images:
+
+
+
+ -
+
+
+
+ 1
+ 0
+
+
+
+
+ -
+
+
+
+ 20
+ 20
+
+
+
+
+ 20
+ 20
+
+
+
+
+
+
+
+
+
+
+
+ QProgressIndicator
+ QWidget
+
+ 1
+
+
+
+
+
diff --git a/dot.cpp b/dot.cpp
new file mode 100644
index 0000000..64bf7e7
--- /dev/null
+++ b/dot.cpp
@@ -0,0 +1,84 @@
+#include
+#include
+#include
+
+#include "imageviewerplateselector.h"
+#include "utils.h"
+#include "dot.h"
+
+constexpr int DOT_WIDTH = 14;
+constexpr int DOT_HEIGHT = 14;
+
+Dot::Dot(QWidget *parent)
+ : QWidget(parent)
+ , inMove(false)
+{
+ setMouseTracking(true);
+ setFixedSize(DOT_WIDTH, DOT_HEIGHT);
+ show();
+}
+
+void Dot::mousePressEvent(QMouseEvent *e)
+{
+ e->accept();
+
+ m_lastMousePoint = e->pos();
+ inMove = true;
+}
+
+void Dot::mouseMoveEvent(QMouseEvent *e)
+{
+ e->accept();
+
+ if(!inMove)
+ return;
+
+ dragTo(e->pos());
+}
+
+void Dot::mouseReleaseEvent(QMouseEvent *e)
+{
+ e->accept();
+
+ dragTo(e->pos());
+
+ emit moved();
+
+ inMove = false;
+}
+
+void Dot::dragTo(const QPoint &pos)
+{
+ ImageViewerPlateSelector *plateSelectorParent = qobject_cast(parent());
+
+ if(!plateSelectorParent)
+ {
+ qWarning("Parent must be of class ImageViewerPlateSelector");
+ return;
+ }
+
+ const QPoint &targetPos = this->pos() + (pos - m_lastMousePoint);
+
+ if(plateSelectorParent->acceptableDotPosition(QRect(targetPos, size())))
+ {
+ move(targetPos);
+ emit moving();
+ }
+}
+
+void Dot::paintEvent(QPaintEvent *e)
+{
+ QPainter painter(this);
+
+ painter.setClipRegion(e->region());
+ painter.setRenderHint(QPainter::Antialiasing);
+
+ painter.setPen(Utils::polygonPaintingPen());
+ painter.setBrush(painter.pen().brush());
+
+ // squeeze the ellipse by the pen width
+ painter.drawEllipse(painter.pen().width(),
+ painter.pen().width(),
+ width() - painter.pen().width()*2,
+ height() - painter.pen().width()*2);
+}
diff --git a/dot.h b/dot.h
new file mode 100644
index 0000000..a133c57
--- /dev/null
+++ b/dot.h
@@ -0,0 +1,83 @@
+#ifndef DOT_H
+#define DOT_H
+
+#include
+#include
+
+/*
+ * Represents a single polygon dot in a plate selector
+ */
+class Dot : public QWidget
+{
+ Q_OBJECT
+
+public:
+ Dot(QWidget *parent = nullptr);
+
+ /*
+ * Center point of the dot in parent's coordinates
+ */
+ void setCenterPoint(const QPoint ¢er);
+
+ /*
+ * Center point
+ */
+ QPoint centerPoint() const;
+
+ /*
+ * Appropriate not scaled point in the original image in pixels
+ */
+ void setPointInImage(const QPoint &point);
+ QPoint pointInImage() const;
+
+protected:
+ virtual void mousePressEvent(QMouseEvent *e) override;
+ virtual void mouseMoveEvent(QMouseEvent *e) override;
+ virtual void mouseReleaseEvent(QMouseEvent *e) override;
+ virtual void paintEvent(QPaintEvent *e) override;
+
+private:
+ QPoint relativeCenter() const;
+ void dragTo(const QPoint &pos);
+
+signals:
+ void moving();
+ void moved();
+
+private:
+ QPoint m_lastMousePoint;
+ QPoint m_pointInImage;
+ bool inMove;
+};
+
+inline
+void Dot::setCenterPoint(const QPoint ¢er)
+{
+ move(center - relativeCenter());
+}
+
+inline
+QPoint Dot::centerPoint() const
+{
+ return pos() + relativeCenter();
+}
+
+inline
+void Dot::setPointInImage(const QPoint &point)
+{
+ m_pointInImage = point;
+}
+
+inline
+QPoint Dot::pointInImage() const
+{
+ return m_pointInImage;
+}
+
+inline
+QPoint Dot::relativeCenter() const
+{
+ return QPoint(width()/2, height()/2);
+}
+
+#endif // DOT_H
diff --git a/imagefile.cpp b/imagefile.cpp
new file mode 100644
index 0000000..9a86f5e
--- /dev/null
+++ b/imagefile.cpp
@@ -0,0 +1,51 @@
+#include
+
+#include "imagefile.h"
+#include "utils.h"
+
+ImageFile::ImageFile()
+{}
+
+ImageFile::ImageFile(const QFileInfo &fileInfo)
+ : m_fileInfo(fileInfo)
+ , m_moved(false)
+{
+ // extract "/data/car" from "/data/car.jpg"
+ m_plateNameTemplate = m_fileInfo.canonicalPath() + QDir::separator() + m_fileInfo.completeBaseName();
+ m_plates = PlateFile::fromImageFile(m_plateNameTemplate);
+}
+
+void ImageFile::setPlates(const PlateFileList &plateFileList)
+{
+ m_plates = plateFileList;
+
+ // sync to disk
+ for(int i = 0;i < m_plates.size();i++)
+ {
+ QString error;
+ PlateFile plateFile = m_plates.at(i);
+
+ plateFile.setImageFile(m_fileInfo.fileName());
+
+ if(!plateFile.writeToFile(i, &error))
+ {
+ Utils::error(QObject::tr("Cannot save YAML file with index %1. %2")
+ .arg(i)
+ .arg(error.isEmpty()
+ ? QObject::tr("Unknown error")
+ : QObject::tr("Error: %1").arg(error)));
+ }
+ }
+
+ int index = m_plates.size();
+
+ while(true)
+ {
+ const QString &file = m_plateNameTemplate + QString("-%1.yaml").arg(index);
+
+ if(!QFile::exists(file))
+ break;
+
+ QFile::remove(file);
+ }
+}
diff --git a/imagefile.h b/imagefile.h
new file mode 100644
index 0000000..c4d3e27
--- /dev/null
+++ b/imagefile.h
@@ -0,0 +1,91 @@
+#ifndef IMAGEFILE_H
+#define IMAGEFILE_H
+
+#include
+#include
+
+#include "platefile.h"
+
+/*
+ * Represents a single image with associated YAML files
+ *
+ * If an image file is "car.jpg", associated YAML files will be named
+ *
+ * car-0.yaml
+ * car-1.yaml
+ * etc.
+ */
+class ImageFile
+{
+public:
+ ImageFile();
+ ImageFile(const QFileInfo &fileInfo);
+
+ /*
+ * Image file info
+ */
+ const QFileInfo &fileInfo() const;
+
+ /*
+ * Plate name template
+ */
+
+ QString plateNameTemplate() const;
+
+ /*
+ * Associated plates
+ */
+ PlateFileList plates() const;
+
+ /*
+ * Update plates
+ */
+ void setPlates(const PlateFileList &plateFileList);
+
+ /*
+ * Image has been moved and its methods and properties
+ * should not be used anymore
+ */
+ bool moved() const;
+ void setMoved(bool moved);
+
+private:
+ QFileInfo m_fileInfo;
+ QString m_plateNameTemplate;
+ PlateFileList m_plates;
+ bool m_moved;
+};
+
+inline
+const QFileInfo &ImageFile::fileInfo() const
+{
+ return m_fileInfo;
+}
+
+inline
+QString ImageFile::plateNameTemplate() const
+{
+ return m_plateNameTemplate;
+}
+
+inline
+PlateFileList ImageFile::plates() const
+{
+ return m_plates;
+}
+
+inline
+bool ImageFile::moved() const
+{
+ return m_moved;
+}
+
+inline
+void ImageFile::setMoved(bool moved)
+{
+ m_moved = moved;
+}
+
+using ImageFileList = QList;
+
+#endif // IMAGEFILE_H
diff --git a/images/plates.icns b/images/plates.icns
new file mode 100644
index 0000000..8ea3ba0
Binary files /dev/null and b/images/plates.icns differ
diff --git a/images/plates.ico b/images/plates.ico
new file mode 100644
index 0000000..2893c8b
Binary files /dev/null and b/images/plates.ico differ
diff --git a/imageview.cpp b/imageview.cpp
new file mode 100644
index 0000000..768cccf
--- /dev/null
+++ b/imageview.cpp
@@ -0,0 +1,86 @@
+#include
+#include
+#include
+
+#include "threadedimageloader.h"
+#include "QProgressIndicator.h"
+#include "imagefile.h"
+#include "imageview.h"
+#include "utils.h"
+#include "ui_imageview.h"
+
+ImageView::ImageView(QWidget *parent)
+ : QStackedWidget(parent)
+ , ui(new Ui::ImageView)
+ , m_imageFile(nullptr)
+ , m_threadedImageLoader(new ThreadedImageLoader(this))
+{
+ ui->setupUi(this);
+
+ // the image has been loaded
+ connect(m_threadedImageLoader, SIGNAL(loaded(QImage)), this, SLOT(slotImageFileLoaded(QImage)));
+
+ // proxying signals
+ connect(ui->imageViewer, SIGNAL(zoomChanged(double)), this, SIGNAL(zoomChanged(double)));
+ connect(ui->imageViewer, SIGNAL(platesChanged()), this, SIGNAL(platesChanged()));
+}
+
+ImageView::~ImageView()
+{
+ delete ui;
+}
+
+void ImageView::setImageFile(ImageFile *imageFile)
+{
+ qDebug("Loading image '%s'", qPrintable(imageFile->fileInfo().canonicalFilePath()));
+
+ if(busy())
+ return;
+
+ m_imageFile = imageFile;
+
+ setCurrentIndex(0);
+ ui->progress->startAnimation();
+
+ m_threadedImageLoader->loadImageFromFile(imageFile->fileInfo().canonicalFilePath());
+}
+
+bool ImageView::busy() const
+{
+ return m_threadedImageLoader->isRunning();
+}
+
+void ImageView::reset()
+{
+ if(busy())
+ return;
+
+ setCurrentIndex(0);
+
+ m_imageFile = nullptr;
+ ui->imageViewer->reset();
+}
+
+void ImageView::setImage(const QImage &image)
+{
+ if(!m_imageFile)
+ return;
+
+ setCurrentIndex(1);
+ ui->progress->stopAnimation();
+
+ ui->imageViewer->setImageFile(m_imageFile);
+ ui->imageViewer->setImage(image);
+}
+
+void ImageView::slotImageFileLoaded(const QImage &image)
+{
+ if(image.isNull())
+ {
+ setCurrentIndex(1);
+ Utils::error(tr("Cannot load the specified image. Possibly it's corrupted"), this);
+ return;
+ }
+
+ setImage(image);
+}
diff --git a/imageview.h b/imageview.h
new file mode 100644
index 0000000..4ad578a
--- /dev/null
+++ b/imageview.h
@@ -0,0 +1,60 @@
+#ifndef IMAGEVIEW_H
+#define IMAGEVIEW_H
+
+#include
+#include
+
+class QImage;
+
+class ImageFile;
+
+namespace Ui
+{
+ class ImageView;
+}
+
+class ThreadedImageLoader;
+
+/*
+ * Image viewer preserving aspect ratio and supporting scaling.
+ *
+ * Displays a spinning progress when loading a file. Utilizes ImageViewer class
+ * to display an actual image
+ */
+class ImageView : public QStackedWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ImageView(QWidget *parent = 0);
+ ~ImageView();
+
+ /*
+ * Load the specified image
+ */
+ void setImageFile(ImageFile *imageFile);
+
+ /*
+ * Is loading the specified image
+ */
+ bool busy() const;
+
+ void reset();
+
+private:
+ void setImage(const QImage &image);
+
+signals:
+ void zoomChanged(double);
+ void platesChanged();
+
+private slots:
+ void slotImageFileLoaded(const QImage &image);
+
+private:
+ Ui::ImageView *ui;
+ ImageFile *m_imageFile;
+ ThreadedImageLoader *m_threadedImageLoader;
+};
+
+#endif // IMAGEVIEW_H
diff --git a/imageview.ui b/imageview.ui
new file mode 100644
index 0000000..88764ef
--- /dev/null
+++ b/imageview.ui
@@ -0,0 +1,109 @@
+
+
+ ImageView
+
+
+
+ 0
+ 0
+ 389
+ 300
+
+
+
+ QFrame::StyledPanel
+
+
+ 0
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 120
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 165
+ 20
+
+
+
+
+ -
+
+
+
+ 24
+ 24
+
+
+
+
+ 24
+ 24
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 164
+ 20
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 120
+
+
+
+
+
+
+
+
+
+
+ ImageViewerObserver
+ QWidget
+
+ 1
+
+
+ QProgressIndicator
+ QWidget
+
+ 1
+
+
+
+
+
diff --git a/imageviewerbase.cpp b/imageviewerbase.cpp
new file mode 100644
index 0000000..4f8ef95
--- /dev/null
+++ b/imageviewerbase.cpp
@@ -0,0 +1,88 @@
+#include
+#include
+#include
+
+#include "imageviewerbase.h"
+
+ImageViewerBase::ImageViewerBase(QWidget *parent)
+ : QWidget(parent)
+ , m_zoom(1)
+{
+ m_timerDelayedUpdate = new QTimer(this);
+ m_timerDelayedUpdate->setSingleShot(true);
+ m_timerDelayedUpdate->setInterval(10);
+ connect(m_timerDelayedUpdate, SIGNAL(timeout()), this, SLOT(updatePixmap()));
+
+ setMinimumSize(200, 120);
+}
+
+void ImageViewerBase::setImage(const QImage &image)
+{
+ m_timerDelayedUpdate->stop();
+ m_image = image;
+
+ updatePixmap();
+}
+
+void ImageViewerBase::reset()
+{
+ m_image = QImage();
+ m_pixmap = QPixmap();
+ m_pixmapBoundingRect = QRect();
+ m_zoom = 1;
+
+ update();
+}
+
+void ImageViewerBase::resizeEvent(QResizeEvent *event)
+{
+ Q_UNUSED(event)
+
+ if(!m_timerDelayedUpdate->isActive())
+ m_timerDelayedUpdate->start();
+}
+
+void ImageViewerBase::paintEvent(QPaintEvent *event)
+{
+ if(m_pixmap.isNull())
+ return;
+
+ QPainter painter(this);
+
+ painter.setClipRegion(event->region());
+ painter.setRenderHint(QPainter::Antialiasing);
+
+ // car image
+ painter.drawPixmap(m_pixmapBoundingRect.x(), m_pixmapBoundingRect.y(), m_pixmap);
+
+ // custom paint in subclasses
+ draw(&painter);
+}
+
+void ImageViewerBase::updatePixmap()
+{
+ pixmapAboutToBeChanged();
+
+ m_pixmap = QPixmap();
+
+ if(m_image.isNull())
+ return;
+
+ // scale with a higher quality
+ m_pixmap = QPixmap::fromImage(m_image.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
+
+ // calculate scaling
+ m_zoom = static_cast(m_pixmap.width()) / m_image.width();
+
+ // pixmap bounding rectangle
+ m_pixmapBoundingRect = QRect((width() - m_pixmap.width()) / 2,
+ (height() - m_pixmap.height()) / 2,
+ m_pixmap.width(),
+ m_pixmap.height());
+
+ pixmapChanged();
+
+ emit zoomChanged(m_zoom);
+
+ update();
+}
diff --git a/imageviewerbase.h b/imageviewerbase.h
new file mode 100644
index 0000000..9cc5bae
--- /dev/null
+++ b/imageviewerbase.h
@@ -0,0 +1,102 @@
+#ifndef IMAGEVIEWERBASE_H
+#define IMAGEVIEWERBASE_H
+
+#include
+#include
+#include
+#include
+
+class QPainter;
+class QTimer;
+
+/*
+ * Base image viewer widget with auto scaling
+ */
+class ImageViewerBase : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ImageViewerBase(QWidget *parent = 0);
+ virtual ~ImageViewerBase() = default;
+
+ /*
+ * Display an image in the view
+ */
+ void setImage(const QImage &image);
+
+ /*
+ * Current zoom
+ */
+ double zoom() const;
+
+ /*
+ * Clear and reset view to initial state with no actual image set
+ */
+ virtual void reset();
+
+protected:
+ virtual void resizeEvent(QResizeEvent *event) override;
+ virtual void paintEvent(QPaintEvent *event) override;
+
+ /*
+ * Getters for derived classes
+ */
+ QPixmap pixmap() const;
+ QImage image() const;
+ QRect pixmapBoundingRect() const;
+
+ /*
+ * Pixmap will be scaled right now!
+ */
+ virtual void pixmapAboutToBeChanged() = 0;
+
+ /*
+ * Pixmap has been scaled and all the internal data has been updated
+ */
+ virtual void pixmapChanged() = 0;
+
+ /*
+ * Draw viewer-specific stuff
+ */
+ virtual void draw(QPainter *painter) = 0;
+
+signals:
+ void zoomChanged(double);
+
+private slots:
+ void updatePixmap();
+
+private:
+ QTimer *m_timerDelayedUpdate;
+ QImage m_image;
+ QPixmap m_pixmap;
+ double m_zoom;
+ QRect m_pixmapBoundingRect;
+};
+
+inline
+double ImageViewerBase::zoom() const
+{
+ return m_zoom;
+}
+
+inline
+QPixmap ImageViewerBase::pixmap() const
+{
+ return m_pixmap;
+}
+
+inline
+QImage ImageViewerBase::image() const
+{
+ return m_image;
+}
+
+inline
+QRect ImageViewerBase::pixmapBoundingRect() const
+{
+ return m_pixmapBoundingRect;
+}
+
+#endif // IMAGEVIEWERBASE_H
diff --git a/imageviewerobserver.cpp b/imageviewerobserver.cpp
new file mode 100644
index 0000000..1ed94f5
--- /dev/null
+++ b/imageviewerobserver.cpp
@@ -0,0 +1,262 @@
+#include
+#include
+
+#include "imageviewerobserver.h"
+#include "plateselector.h"
+#include "imagefile.h"
+#include "settings.h"
+
+constexpr double BOUNDING_ENLARGE_WIDTH_FACTOR = 0.15;
+constexpr double BOUNDING_ENLARGE_HEIGHT_FACTOR = 0.25;
+
+ImageViewerObserver::ImageViewerObserver(QWidget *parent)
+ : ImageViewerBase(parent)
+ , m_imageFile(nullptr)
+ , m_plateFileIndexForEditing(-1)
+{}
+
+void ImageViewerObserver::setImageFile(ImageFile *imageFile)
+{
+ // save image info
+ m_imageFile = imageFile;
+
+ // saved license plates
+ m_polygons = Polygons::fromPlates(m_imageFile->plates());
+}
+
+void ImageViewerObserver::reset()
+{
+ m_imageFile = nullptr;
+ m_polygons.clear();
+
+ ImageViewerBase::reset();
+}
+
+void ImageViewerObserver::mousePressEvent(QMouseEvent *e)
+{
+ e->accept();
+
+ // start to paint a selection rectangle
+ m_selection.reset();
+
+ // the point is outside the image or nothing to select
+ if(pixmap().isNull())
+ return;
+
+ m_selection.start(e->pos());
+ update();
+}
+
+void ImageViewerObserver::mouseMoveEvent(QMouseEvent *e)
+{
+ e->accept();
+
+ // no selection or the point is outside the image
+ if(!m_selection.hasSelection())
+ return;
+
+ m_selection.move(e->pos());
+ update();
+}
+
+void ImageViewerObserver::mouseReleaseEvent(QMouseEvent *e)
+{
+ e->accept();
+
+ // no selection/selection is too small, or the point is outside the image
+ if(!m_selection.selectionIsFine())
+ {
+ QPoint pos = (e->pos() - pixmapBoundingRect().topLeft()) / zoom();
+
+ // handle click
+ for(int i = 0;i < m_imageFile->plates().size();i++)
+ {
+ const PlateFile &plateFile = m_imageFile->plates().at(i);
+
+ if(plateFile.plateCorners().containsPoint(pos, Qt::OddEvenFill))
+ {
+ const QRect &boundingRect = plateFile.plateCorners().boundingRect();
+
+ // enlarge
+ m_fixedSelection = boundingRect.adjusted(-BOUNDING_ENLARGE_WIDTH_FACTOR * boundingRect.width(),
+ -BOUNDING_ENLARGE_HEIGHT_FACTOR * boundingRect.height(),
+ BOUNDING_ENLARGE_WIDTH_FACTOR * boundingRect.width(),
+ BOUNDING_ENLARGE_HEIGHT_FACTOR * boundingRect.height());
+
+ m_fixedSelection &= image().rect();
+
+ m_plateFileIndexForEditing = i;
+
+ QTimer::singleShot(0, this, SLOT(slotShowPlateSelector()));
+ break;
+ }
+ }
+
+ return;
+ }
+
+ m_selection.stop();
+
+ m_fixedSelection = m_selection.selection();
+
+ // relative to the image itself
+ m_fixedSelection.moveTopLeft(m_fixedSelection.topLeft() - pixmapBoundingRect().topLeft());
+
+ // take zoom into account
+ m_fixedSelection.moveTopLeft(m_fixedSelection.topLeft() * (1 / zoom()));
+ m_fixedSelection.setSize(m_fixedSelection.size() * (1 / zoom()));
+
+ // fix possible division errors:
+ // - negative coordinates
+ // - too large size
+ m_fixedSelection &= image().rect();
+
+ if(!m_fixedSelection.isValid())
+ {
+ qWarning() << "Selection rectangle" << m_fixedSelection << "is invalid for image size" << image().size();
+ return;
+ }
+
+ qDebug() << "Actual selection: " << m_fixedSelection;
+
+ m_plateFileIndexForEditing = -1;
+
+ QTimer::singleShot(0, this, SLOT(slotShowPlateSelector()));
+
+ update();
+}
+
+bool ImageViewerObserver::event(QEvent *event)
+{
+ if(event->type() == QEvent::ToolTip && SETTINGS_GET_BOOL(SETTING_TOOLTIPS))
+ {
+ const QPoint &pos = (mapFromGlobal(QCursor::pos()) - pixmapBoundingRect().topLeft()) / zoom();
+
+ // handle click
+ foreach(const PlateFile &plateFile, m_imageFile->plates())
+ {
+ if(plateFile.plateCorners().containsPoint(pos, Qt::OddEvenFill))
+ {
+ QToolTip::showText(QCursor::pos(),
+ QString(
+ // ""
+ //"| %1 | %2 |
"
+ //"| %3 | %4 |
"
+ //"| %5 | %6 |
"
+ // "
"
+ "%1 %2
"
+ "%3 %4
"
+ "%5 %6"
+ )
+ .arg(tr("Plate number:"))
+ .arg(plateFile.plateNumber())
+ .arg(tr("Plate region:"))
+ .arg(plateFile.regionCode())
+ .arg(tr("Light-on-Dark:"))
+ .arg(plateFile.plateInverted() ? tr("yes") : tr("no")),
+ nullptr,
+ QRect());
+ break;
+ }
+ }
+
+ event->accept();
+ return true;
+ }
+
+ return QWidget::event(event);
+}
+
+void ImageViewerObserver::pixmapAboutToBeChanged()
+{
+ m_selection.reset();
+}
+
+void ImageViewerObserver::pixmapChanged()
+{
+ m_selection.setReferenceRect(pixmapBoundingRect());
+ m_polygons.setZoom(zoom());
+}
+
+void ImageViewerObserver::draw(QPainter *painter)
+{
+ if(!painter)
+ return;
+
+ painter->translate(pixmapBoundingRect().topLeft());
+ m_polygons.draw(painter);
+
+ painter->resetTransform();
+ m_selection.draw(painter);
+}
+
+void ImageViewerObserver::slotShowPlateSelector()
+{
+ qDebug() << "Image selection:" << m_fixedSelection;
+
+ const QImage &frame = image().copy(m_fixedSelection);
+
+ if(frame.isNull())
+ {
+ qWarning() << "Cannot edit a null image from region" << m_fixedSelection;
+ return;
+ }
+
+ PlateSelector selector(frame, this);
+
+ selector.setSelectionRect(m_fixedSelection);
+
+ if(m_plateFileIndexForEditing >= 0)
+ selector.setPlateFile(m_imageFile->plates().at(m_plateFileIndexForEditing));
+
+ if(selector.exec() != PlateSelector::Accepted)
+ return;
+
+ PlateFileList plateFileList = m_imageFile->plates();
+ PlateFile *plateFile = nullptr;
+
+ // existing plate
+ if(m_plateFileIndexForEditing >= 0)
+ {
+ qDebug("Editing existing plate index %d", m_plateFileIndexForEditing);
+
+ // empty polygon -> delete it
+ if(selector.selectedPolygon().isEmpty())
+ plateFileList.removeAt(m_plateFileIndexForEditing);
+ else
+ plateFile = &plateFileList[m_plateFileIndexForEditing];
+ }
+ // new non-empty plate
+ else if(!selector.selectedPolygon().isEmpty())
+ {
+ qDebug("Adding new plate with index %d", plateFileList.size());
+
+ PlateFile newPlateFile(m_imageFile->plateNameTemplate(), plateFileList.size());
+
+ newPlateFile.setImageFile(m_imageFile->fileInfo().fileName());
+ newPlateFile.setImageWidth(image().width());
+ newPlateFile.setImageHeight(image().height());
+
+ plateFileList.append(newPlateFile);
+ plateFile = &plateFileList.last();
+ }
+ else
+ return;
+
+ // update with entered data
+ if(plateFile)
+ {
+ plateFile->setPlateCorners(selector.selectedPolygon());
+ plateFile->setRegionCode(selector.plateRegion());
+ plateFile->setPlateNumber(selector.plateNumber());
+ plateFile->setPlateInverted(selector.lightOnDark());
+ }
+
+ m_imageFile->setPlates(plateFileList);
+
+ emit platesChanged();
+
+ m_polygons = Polygons::fromPlates(m_imageFile->plates());
+ m_polygons.setZoom(zoom());
+ update();
+}
diff --git a/imageviewerobserver.h b/imageviewerobserver.h
new file mode 100644
index 0000000..05880fa
--- /dev/null
+++ b/imageviewerobserver.h
@@ -0,0 +1,62 @@
+#ifndef IMAGEVIEWEROBSERVER_H
+#define IMAGEVIEWEROBSERVER_H
+
+#include "imageviewerbase.h"
+#include "selection.h"
+#include "polygons.h"
+
+class ImageFile;
+
+/*
+ * Image viewer to display a car with static license plates
+ */
+class ImageViewerObserver : public ImageViewerBase
+{
+ Q_OBJECT
+
+public:
+ explicit ImageViewerObserver(QWidget *parent = nullptr);
+
+ /*
+ * Add information with static license plates
+ */
+ void setImageFile(ImageFile *imageFile);
+
+ /*
+ * Reset override
+ */
+ virtual void reset() override;
+
+protected:
+ /*
+ * For handling selection
+ */
+ virtual void mousePressEvent(QMouseEvent *e) override;
+ virtual void mouseMoveEvent(QMouseEvent *e) override;
+ virtual void mouseReleaseEvent(QMouseEvent *e) override;
+
+ /*
+ * Custom tooltips
+ */
+ virtual bool event(QEvent *event) override;
+
+ virtual void pixmapAboutToBeChanged() override;
+ virtual void pixmapChanged() override;
+
+ virtual void draw(QPainter *painter) override;
+
+signals:
+ void platesChanged();
+
+private slots:
+ void slotShowPlateSelector();
+
+private:
+ ImageFile *m_imageFile;
+ Polygons m_polygons;
+ Selection m_selection;
+ QRect m_fixedSelection;
+ int m_plateFileIndexForEditing;
+};
+
+#endif // IMAGEVIEWEROBSERVER_H
diff --git a/imageviewerplateselector.cpp b/imageviewerplateselector.cpp
new file mode 100644
index 0000000..605a936
--- /dev/null
+++ b/imageviewerplateselector.cpp
@@ -0,0 +1,212 @@
+#include
+#include
+#include
+
+#include
+
+#include "imageviewerplateselector.h"
+#include "plateselector.h"
+#include "utils.h"
+#include "dot.h"
+
+ImageViewerPlateSelector::ImageViewerPlateSelector(QWidget *parent)
+ : ImageViewerBase(parent)
+{}
+
+bool ImageViewerPlateSelector::acceptableDotPosition(const QRect &rect) const
+{
+ return rect.intersected(pixmapBoundingRect()) == rect;
+}
+
+bool ImageViewerPlateSelector::addDotInViewCoordinates(const QPoint &point)
+{
+ // only 4 dots are allowed
+ if(m_dots.size() == 4)
+ return false;
+
+ // add new selection dot
+ Dot *dot = new Dot(this);
+
+ connect(dot, SIGNAL(moving()), this, SLOT(slotDotMoving()));
+ connect(dot, SIGNAL(moved()), this, SLOT(slotDotMoved()));
+
+ dot->setCenterPoint(point);
+ updatePointInImage(dot);
+
+ m_dots.append(dot);
+
+ if(!reorderDots())
+ update();
+
+ emitDotsChanged();
+
+ return true;
+}
+
+bool ImageViewerPlateSelector::addDotInImageCoordinates(const QPoint &point)
+{
+ qDebug() << "Add dot" << point;
+ return addDotInViewCoordinates(pixmapBoundingRect().topLeft() + point * zoom());
+}
+
+void ImageViewerPlateSelector::clearDots()
+{
+ if(m_dots.isEmpty())
+ return;
+
+ qDeleteAll(m_dots);
+ m_dots.clear();
+ update();
+
+ emitDotsChanged();
+}
+
+QPolygon ImageViewerPlateSelector::selectedPolygon() const
+{
+ QPolygon polygon;
+
+ if(m_dots.size() != 4)
+ return polygon;
+
+ polygon.reserve(m_dots.size());
+
+ foreach(Dot *dot, m_dots)
+ {
+ polygon.append(dot->pointInImage());
+ }
+
+ return polygon;
+}
+
+void ImageViewerPlateSelector::mousePressEvent(QMouseEvent *e)
+{
+ e->accept();
+}
+
+void ImageViewerPlateSelector::mouseMoveEvent(QMouseEvent *e)
+{
+ e->accept();
+}
+
+void ImageViewerPlateSelector::mouseReleaseEvent(QMouseEvent *e)
+{
+ e->accept();
+
+ // not interesting click
+ if(!pixmapBoundingRect().contains(e->pos()))
+ return;
+
+ addDotInViewCoordinates(e->pos());
+}
+
+void ImageViewerPlateSelector::pixmapAboutToBeChanged()
+{}
+
+void ImageViewerPlateSelector::pixmapChanged()
+{
+ foreach(Dot *dot, m_dots)
+ {
+ dot->setCenterPoint(pixmapBoundingRect().topLeft() + dot->pointInImage() * zoom());
+ }
+}
+
+void ImageViewerPlateSelector::draw(QPainter *painter)
+{
+ if(!painter)
+ return;
+
+ // draw only a full polygon
+ if(m_dots.size() == 4)
+ {
+ painter->setPen(Utils::polygonPaintingPen());
+
+ QPolygon polygon;
+ polygon.reserve(m_dots.size());
+
+ foreach(Dot *dot, m_dots)
+ {
+ polygon.append(dot->centerPoint());
+ }
+
+ painter->drawPolygon(polygon);
+ }
+}
+
+void ImageViewerPlateSelector::updatePointInImage(Dot *dot) const
+{
+ if(!dot)
+ return;
+
+ dot->setPointInImage((dot->centerPoint() - pixmapBoundingRect().topLeft()) / zoom());
+}
+
+void ImageViewerPlateSelector::emitDotsChanged()
+{
+ emit dotsChanged(m_dots.size());
+}
+
+bool ImageViewerPlateSelector::reorderDots()
+{
+ // sort dots to make a valid region
+ if(m_dots.size() != 4)
+ return false;
+
+ QList newDots;
+ newDots.reserve(m_dots.size());
+
+ struct Point
+ {
+ int index;
+ QPointF point;
+ };
+
+ const QPoint ¢roid = Utils::centroid(selectedPolygon());
+ const QPolygon &selectedPolygon = this->selectedPolygon();
+
+ QVector polarPolygon;
+ polarPolygon.reserve(selectedPolygon.size());
+
+ for(int i = 0;i < selectedPolygon.size();i++)
+ {
+ const QPoint &relativeToCentroid = selectedPolygon.at(i) - centroid;
+
+ // convert to polar:
+ // x == theta (angle)
+ // y == r (length)
+ polarPolygon.append( { i, { qAtan2(relativeToCentroid.y(), relativeToCentroid.x()),
+ qSqrt(qPow(relativeToCentroid.x(),2) + qPow(relativeToCentroid.y(),2)) } } );
+ }
+
+ std::sort(polarPolygon.begin(),
+ polarPolygon.end(),
+ [](const Point &a, const Point &b) {
+ return (a.point.x() == b.point.x()) ? a.point.y() < b.point.y() : a.point.x() < b.point.x();
+ });
+
+ foreach(const Point &point, polarPolygon)
+ {
+ newDots.append(m_dots.at(point.index));
+ }
+
+ m_dots = newDots;
+
+ update();
+
+ return true;
+}
+
+void ImageViewerPlateSelector::slotDotMoving()
+{
+ Dot *dot = qobject_cast(sender());
+
+ if(!dot)
+ return;
+
+ updatePointInImage(dot);
+ update();
+}
+
+void ImageViewerPlateSelector::slotDotMoved()
+{
+ reorderDots();
+}
diff --git a/imageviewerplateselector.h b/imageviewerplateselector.h
new file mode 100644
index 0000000..cbb662a
--- /dev/null
+++ b/imageviewerplateselector.h
@@ -0,0 +1,73 @@
+#ifndef IMAGEVIEWERPLATESELECTOR_H
+#define IMAGEVIEWERPLATESELECTOR_H
+
+#include
+#include
+
+#include "imageviewerbase.h"
+
+class Dot;
+
+/*
+ * Image viewer to display a car with static license plates
+ */
+class ImageViewerPlateSelector : public ImageViewerBase
+{
+ Q_OBJECT
+
+public:
+ explicit ImageViewerPlateSelector(QWidget *parent = nullptr);
+
+ /*
+ * Check if the given dot geometry fits into the pixmap
+ */
+ bool acceptableDotPosition(const QRect &rect) const;
+
+ /*
+ * Add a new dot at the given point
+ */
+ bool addDotInViewCoordinates(const QPoint &point);
+ bool addDotInImageCoordinates(const QPoint &point);
+
+ /*
+ * Remove all dots
+ */
+ void clearDots();
+
+ /*
+ * Selection result as a polygon with 4 points. Each point is
+ * in image coordinates
+ */
+ QPolygon selectedPolygon() const;
+
+protected:
+ /*
+ * For handling selection
+ */
+ virtual void mousePressEvent(QMouseEvent *e) override;
+ virtual void mouseMoveEvent(QMouseEvent *e) override;
+ virtual void mouseReleaseEvent(QMouseEvent *e) override;
+
+ virtual void pixmapAboutToBeChanged() override;
+ virtual void pixmapChanged() override;
+
+ virtual void draw(QPainter *painter) override;
+
+private:
+ void updatePointInImage(Dot *dot) const;
+ void emitDotsChanged();
+ bool reorderDots();
+
+signals:
+ void dotsChanged(int);
+
+private slots:
+ void slotDotMoving();
+ void slotDotMoved();
+
+private:
+ // selection dots
+ QList m_dots;
+};
+
+#endif // IMAGEVIEWERPLATESELECTOR_H
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 0000000..10e899c
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,117 @@
+#include
+
+#include
+
+#include "mainwindow.h"
+
+static char messageTypeToChar(QtMsgType type)
+{
+ switch(type)
+ {
+ case QtDebugMsg: return 'D';
+ case QtWarningMsg: return 'W';
+ case QtCriticalMsg: return 'C';
+ case QtFatalMsg: return 'F';
+#if QT_VERSION >= QT_VERSION_CHECK(5,5,0)
+ case QtInfoMsg: return 'I';
+#endif
+
+ default:
+ return 'U';
+ }
+}
+
+static void platesOutput(QtMsgType type, const QMessageLogContext &context, const QString &message)
+{
+ Q_UNUSED(context)
+
+ QByteArray msg = message.toLatin1();
+
+ const QString currentTime = QDateTime::currentDateTime().toString("hh:mm:ss.zzz");
+
+ const char messageChar = messageTypeToChar(type);
+
+ fprintf(stderr, "%s %c %s\n", qPrintable(currentTime), messageChar, static_cast(msg));
+
+#ifndef PLATES_NO_LOG
+ static bool noLog = qgetenv("PLATES_NO_LOG") == "1";
+
+ if(noLog)
+ return;
+
+ static QFile log(
+ QStandardPaths::writableLocation(QStandardPaths::TempLocation)
+ + QDir::separator()
+ + QCoreApplication::applicationName().toLower()
+ + ".log");
+
+ static bool failed = false;
+
+ if(!log.isOpen() && !failed)
+ {
+ failed = !log.open(QIODevice::ReadWrite | QIODevice::Append) || !log.resize(0);
+
+ if(failed)
+ fprintf(stderr, "Log file is unavailable\n");
+ }
+
+ if(!failed)
+ {
+ // truncate
+ if(log.size() > 1*1024*1024) // 1 Mb
+ {
+ fprintf(stderr, "Truncating log\n");
+
+ log.seek(0);
+
+ char buf[1024];
+ int lines = 0;
+ char c = 'x';
+
+ while(log.readLine(buf, sizeof(buf)) > 0 && lines++ < 30)
+ {}
+
+ log.resize(log.pos());
+
+ // check if '\n' is last
+ if(log.seek(log.pos()-1))
+ {
+ log.getChar(&c);
+
+ if(c != '\n')
+ log.write("\n");
+ }
+
+ log.write("...\n\n...\n");
+ }
+
+ log.write(currentTime.toLatin1());
+ log.write(" ");
+ log.write(&messageChar, sizeof(decltype(messageChar)));
+ log.write(" ");
+ log.write(static_cast(msg));
+ log.write("\n");
+ }
+#endif // PLATES_NO_LOG
+
+ if(type == QtFatalMsg)
+ abort();
+}
+
+int main(int argc, char *argv[])
+{
+ setbuf(stderr, 0);
+
+ QCoreApplication::setApplicationName(TARGET_HUMAN_STRING);
+ QCoreApplication::setOrganizationName("OpenALPR");
+ QCoreApplication::setApplicationVersion(VERSION_STRING);
+
+ QApplication app(argc, argv);
+
+ qInstallMessageHandler(platesOutput);
+
+ MainWindow w;
+ w.show();
+
+ return app.exec();
+}
diff --git a/mainwindow.cpp b/mainwindow.cpp
new file mode 100644
index 0000000..836af20
--- /dev/null
+++ b/mainwindow.cpp
@@ -0,0 +1,362 @@
+#include
+#include
+#include
+
+#include "directoryloader.h"
+#include "mainwindow.h"
+#include "settings.h"
+#include "options.h"
+#include "about.h"
+#include "utils.h"
+#include "ui_mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+ : QMainWindow(parent)
+ , ui(new Ui::MainWindow)
+ , m_firstRun(true)
+{
+ ui->setupUi(this);
+
+ setWindowTitle(TARGET_HUMAN_STRING);
+
+ // shortcuts
+ ui->actionOpen_directory->setShortcut(QKeySequence::Open);
+ ui->actionQuit->setShortcut(QKeySequence::Quit);
+ ui->actionOptions->setShortcut(QKeySequence::Preferences);
+ ui->actionAbout->setShortcut(QKeySequence::HelpContents);
+
+ // set tooltips
+ Utils::setShortcutTooltips(QList()
+ << ui->pushPrevious
+ << ui->pushJumpTo
+ << ui->pushNext);
+
+ // zoom
+ connect(ui->imageView, SIGNAL(zoomChanged(double)), this, SLOT(slotZoomChanged(double)));
+ connect(ui->imageView, SIGNAL(platesChanged()), this, SLOT(slotPlatesChanged()));
+
+ // open a directory at startup
+ QTimer::singleShot(0, this, SLOT(slotOpenDirectory()));
+}
+
+MainWindow::~MainWindow()
+{
+ delete ui;
+}
+
+void MainWindow::reset()
+{
+ m_imageFiles.clear();
+ m_currentIndex = -1;
+
+ ui->imageView->reset();
+ ui->labelImageNumber->clear();
+ ui->labelImageFile->clear();
+ ui->labelCurrentImageInfo->clear();
+ ui->labelZoom->clear();
+
+ ui->labelImagesTagged->clear();
+ ui->labelTotalPlates->clear();
+}
+
+void MainWindow::updateImageInformationFromIndex()
+{
+ if(m_currentIndex >= 0 && m_currentIndex < m_imageFiles.size())
+ {
+ ImageFile &imageFile = m_imageFiles[m_currentIndex];
+
+ ui->imageView->setImageFile(&imageFile);
+ ui->labelImageFile->setText(imageFile.fileInfo().fileName());
+ ui->labelImageNumber->setText(QString("%1 / %2").arg(m_currentIndex+1).arg(m_imageFiles.size()));
+
+ updateImagePlatesInformationFromIndex();
+ }
+ else
+ {
+ QStringList message = QStringList() << tr("No suitable images available");
+
+ if(SETTINGS_GET_BOOL(SETTING_SKIP_TAGGED))
+ message.append(tr("Tagged images have been skipped"));
+
+ if(SETTINGS_GET_BOOL(SETTING_MOVE_UNTAGGED))
+ message.append(tr("Untagged images have been moved into \"negatives\" subdirectory"));
+
+ Utils::message(message.join(". "), this);
+
+ ui->imageView->reset();
+ ui->labelImageNumber->setText(QString("- / %1").arg(m_imageFiles.size()));
+ ui->labelImageFile->clear();
+ ui->labelCurrentImageInfo->clear();
+ ui->labelZoom->clear();
+ }
+}
+
+void MainWindow::updateImagePlatesInformationFromIndex()
+{
+ if(m_currentIndex < 0 || m_currentIndex >= m_imageFiles.size())
+ {
+ ui->labelCurrentImageInfo->clear();
+ return;
+ }
+
+ ui->labelCurrentImageInfo->setText(m_imageFiles[m_currentIndex].plates().isEmpty()
+ ? tr("Untagged: Drag a box around a license plate to tag it")
+ : tr("Number of tagged license plates: %1")
+ .arg(m_imageFiles[m_currentIndex].plates().size()));
+}
+
+void MainWindow::updateTaggedAndPlatesInformation()
+{
+ int tagged = 0;
+ int plates = 0;
+
+ foreach(const ImageFile &imageFile, m_imageFiles)
+ {
+ tagged += !imageFile.plates().isEmpty();
+ plates += imageFile.plates().size();
+ }
+
+ ui->labelImagesTagged->setNum(tagged);
+ ui->labelTotalPlates->setNum(plates);
+}
+
+void MainWindow::moveFileFromCurrentIndex()
+{
+ // return if:
+ // - moving disabled
+ // - invalid index
+ // - currently selected image has plates
+ if(!SETTINGS_GET_BOOL(SETTING_MOVE_UNTAGGED)
+ || m_currentIndex < 0
+ || m_currentIndex >= m_imageFiles.size()
+ || !m_imageFiles.at(m_currentIndex).plates().isEmpty())
+ return;
+
+ const QFileInfo &imageFileInfo = m_imageFiles.at(m_currentIndex).fileInfo();
+
+ qDebug("Move '%s'", qPrintable(imageFileInfo.fileName()));
+
+ QDir negatives(imageFileInfo.canonicalPath());
+
+ if(!negatives.mkpath("negatives"))
+ {
+ Utils::error(tr("Cannot create \"negatives\" subdirectory"), this);
+ return;
+ }
+
+ negatives.cd("negatives");
+
+ const QString &target = negatives.absoluteFilePath(imageFileInfo.fileName());
+
+ if(QFile::exists(target) && !QFile::remove(target))
+ {
+ Utils::error(tr("Cannot remove the file \"%1\" in \"negatives\" subdirectory").arg(imageFileInfo.fileName()), this);
+ return;
+ }
+
+ if(!QFile::rename(imageFileInfo.canonicalFilePath(), target))
+ {
+ Utils::error(tr("Cannot move the file \"%1\" into \"negatives\" subdirectory").arg(imageFileInfo.fileName()), this);
+ return;
+ }
+
+ m_imageFiles[m_currentIndex].setMoved(true);
+}
+
+void MainWindow::slotOpenDirectory()
+{
+ qDebug("Open directory");
+
+ ImageFileList list;
+
+ while(true)
+ {
+ const QString &dir = QFileDialog::getExistingDirectory(this,
+ tr("Select a directory"),
+ SETTINGS_GET_STRING(SETTING_LAST_DIRECTORY));
+
+ if(dir.isEmpty())
+ {
+ if(QMessageBox::question(this,
+ QString(),
+ tr("No directory has been selected. Whould you like to try again?")) == QMessageBox::Yes)
+ continue;
+ else
+ {
+ // in auto-open mode quit immediately
+ if(m_firstRun)
+ close();
+
+ return;
+ }
+ }
+
+ QFileInfo dirInfo(dir);
+
+ SETTINGS_SET_STRING(SETTING_LAST_DIRECTORY, dir);
+
+ if(!dirInfo.isWritable())
+ {
+ Utils::warning(tr("The selected directory is not writable. Please select another directory"));
+ continue;
+ }
+
+ qDebug("Loading directory '%s'", qPrintable(dir));
+
+ DirectoryLoader loader(dir, this);
+
+ if(loader.exec() != DirectoryLoader::Accepted)
+ {
+ Utils::warning(tr("The selected directory doesn't contain JPEG or PNG images. Please select another directory"));
+ continue;
+ }
+
+ list = loader.imageFiles();
+ m_firstRun = false;
+
+ break;
+ }
+
+ reset();
+
+ m_imageFiles = list;
+
+ qDebug("Loaded files: %d", m_imageFiles.size());
+
+ updateTaggedAndPlatesInformation();
+
+ // load the next available image
+ slotNext();
+}
+
+void MainWindow::slotQuit()
+{
+ close();
+}
+
+void MainWindow::slotOptions()
+{
+ qDebug("Options");
+
+ Options options(this);
+ options.exec();
+}
+
+void MainWindow::slotUsage()
+{
+ qDebug("Usage");
+
+ Utils::help(tr("Drag a box around each plate in the image to tag it. "
+ "If the image has no plates, click Next to skip it. "
+ "If the option \"Move images without plates\" is selected, the image will be moved into \"negatives\" subdirectory.
"
+ "Hotkeys:"
+ ""
+ "- %1 - Previous"
+ "
- %2 - Next"
+ "
- %3 - Jump To..."
+ "
")
+ .arg(ui->pushPrevious->shortcut().toString())
+ .arg(ui->pushNext->shortcut().toString())
+ .arg(ui->pushJumpTo->shortcut().toString())
+ , this);
+}
+
+void MainWindow::slotAbout()
+{
+ About about(this);
+ about.exec();
+}
+
+void MainWindow::slotAboutQt()
+{
+ QMessageBox::aboutQt(this);
+}
+
+void MainWindow::slotPrevious()
+{
+ qDebug("Previous");
+
+ // nothing to load
+ if(m_currentIndex <= 0 || ui->imageView->busy())
+ return;
+
+ do {
+ m_currentIndex--;
+ }while(m_currentIndex >= 0 && m_imageFiles.at(m_currentIndex).moved());
+
+ updateImageInformationFromIndex();
+}
+
+void MainWindow::slotJumpTo()
+{
+ qDebug("Jump to");
+
+ if(ui->imageView->busy())
+ return;
+
+ bool ok;
+ int index = QInputDialog::getInt(this,
+ tr("Jump To"),
+ tr("Image number:"),
+ m_currentIndex+1, // current value
+ 1, // min
+ m_imageFiles.size(), // max
+ 1,
+ &ok);
+
+ if(ok)
+ {
+ int newIndex = index-1;
+
+ if(m_imageFiles.at(newIndex).moved())
+ {
+ Utils::message(tr("The specified image has been moved into \"negatives\" subdirectory and no longer available"), this);
+ return;
+ }
+
+ m_currentIndex = newIndex;
+ updateImageInformationFromIndex();
+ }
+}
+
+void MainWindow::slotNext()
+{
+ qDebug("Next");
+
+ // nothing to load
+ if(m_currentIndex >= m_imageFiles.size() || ui->imageView->busy())
+ {
+ qDebug("Nothing to load");
+ return;
+ }
+
+ // move if necessary
+ moveFileFromCurrentIndex();
+
+ // last image and it has not been moved
+ if(m_currentIndex == m_imageFiles.size()-1 && !m_imageFiles.at(m_currentIndex).moved())
+ return;
+
+ // not last or moved
+ do {
+ m_currentIndex++;
+ }while(m_currentIndex < m_imageFiles.size()
+ && (m_imageFiles.at(m_currentIndex).moved()
+ || (SETTINGS_GET_BOOL(SETTING_SKIP_TAGGED) && !m_imageFiles.at(m_currentIndex).plates().isEmpty()))
+ );
+
+ updateImageInformationFromIndex();
+}
+
+void MainWindow::slotZoomChanged(double zoom)
+{
+ ui->labelZoom->setText(QString("%1x (%2%)")
+ .arg(zoom, 0, 'f', 1)
+ .arg(zoom * 100, 0, 'f', 1));
+}
+
+void MainWindow::slotPlatesChanged()
+{
+ updateImagePlatesInformationFromIndex();
+ updateTaggedAndPlatesInformation();
+}
diff --git a/mainwindow.h b/mainwindow.h
new file mode 100644
index 0000000..c9eb0e5
--- /dev/null
+++ b/mainwindow.h
@@ -0,0 +1,73 @@
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include
+
+#include "imagefile.h"
+
+class QImage;
+
+namespace Ui
+{
+ class MainWindow;
+}
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ explicit MainWindow(QWidget *parent = 0);
+ ~MainWindow();
+
+private:
+ /*
+ * Reset all labels
+ */
+ void reset();
+
+ /*
+ * Update labels based on the currently selected image
+ */
+ void updateImageInformationFromIndex();
+ void updateImagePlatesInformationFromIndex();
+
+ /*
+ * Update labels with a number of tagged images
+ */
+ void updateTaggedAndPlatesInformation();
+
+ /*
+ * Move the currently selected image file to negatives
+ * if necessary
+ */
+ void moveFileFromCurrentIndex();
+
+private slots:
+ // File menu
+ void slotOpenDirectory();
+ void slotQuit();
+
+ // Edit menu
+ void slotOptions();
+
+ // Help menu
+ void slotUsage();
+ void slotAbout();
+ void slotAboutQt();
+
+ // other slots
+ void slotPrevious();
+ void slotJumpTo();
+ void slotNext();
+ void slotZoomChanged(double zoom);
+ void slotPlatesChanged();
+
+private:
+ Ui::MainWindow *ui;
+ ImageFileList m_imageFiles;
+ int m_currentIndex;
+ bool m_firstRun;
+};
+
+#endif // MAINWINDOW_H
diff --git a/mainwindow.ui b/mainwindow.ui
new file mode 100644
index 0000000..3ca93a2
--- /dev/null
+++ b/mainwindow.ui
@@ -0,0 +1,501 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 800
+ 550
+
+
+
+
+
+ 10
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 0
+
+
+
+
+ -
+
+
-
+
+
+
+ 1
+ 0
+
+
+
+
+ 75
+ true
+
+
+
+ Images tagged:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Total plates:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 0
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 0
+
+
+
+
+ -
+
+
+ Previous
+
+
+ P
+
+
+
+ -
+
+
+ Jump To...
+
+
+ J
+
+
+
+ -
+
+
+ Next
+
+
+ N
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 75
+ true
+
+
+
+ Image
+
+
+
+ -
+
+
+
+ 1
+ 0
+
+
+
+
+ 75
+ true
+
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 1
+
+
+
+
+ -
+
+
-
+
+
+
+ 1
+ 0
+
+
+
+
+ 75
+ true
+
+
+
+
+ -
+
+
+ Zoom:
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+ Open directory...
+
+
+
+
+ Preferences...
+
+
+
+
+ About...
+
+
+
+
+ Quit
+
+
+
+
+ Usage...
+
+
+
+
+ About Qt...
+
+
+
+
+
+
+ ImageView
+ QWidget
+
+ 1
+
+
+ SqueezedLabel
+ QLabel
+
+
+
+
+
+
+ actionOpen_directory
+ triggered()
+ MainWindow
+ slotOpenDirectory()
+
+
+ -1
+ -1
+
+
+ 199
+ 149
+
+
+
+
+ actionOptions
+ triggered()
+ MainWindow
+ slotOptions()
+
+
+ -1
+ -1
+
+
+ 199
+ 149
+
+
+
+
+ actionAbout
+ triggered()
+ MainWindow
+ slotAbout()
+
+
+ -1
+ -1
+
+
+ 320
+ 207
+
+
+
+
+ pushPrevious
+ clicked()
+ MainWindow
+ slotPrevious()
+
+
+ 356
+ 68
+
+
+ 223
+ 466
+
+
+
+
+ pushJumpTo
+ clicked()
+ MainWindow
+ slotJumpTo()
+
+
+ 437
+ 68
+
+
+ 306
+ 437
+
+
+
+
+ pushNext
+ clicked()
+ MainWindow
+ slotNext()
+
+
+ 518
+ 68
+
+
+ 384
+ 490
+
+
+
+
+ actionQuit
+ triggered()
+ MainWindow
+ slotQuit()
+
+
+ -1
+ -1
+
+
+ 338
+ 210
+
+
+
+
+ actionUsage
+ triggered()
+ MainWindow
+ slotUsage()
+
+
+ -1
+ -1
+
+
+ 338
+ 210
+
+
+
+
+ actionAbout_Qt
+ triggered()
+ MainWindow
+ slotAboutQt()
+
+
+ -1
+ -1
+
+
+ 399
+ 274
+
+
+
+
+
+ slotOpenDirectory()
+ slotOptions()
+ slotAbout()
+ slotPrevious()
+ slotJumpTo()
+ slotNext()
+ slotQuit()
+ slotUsage()
+ slotAboutQt()
+
+
diff --git a/man/plates.1 b/man/plates.1
new file mode 100644
index 0000000..dc2b8fd
--- /dev/null
+++ b/man/plates.1
@@ -0,0 +1,21 @@
+.TH PLATES 1 "APRIL 7, 2017"
+.\" Please adjust this date whenever revising the manpage.
+.SH NAME
+plates \- an OpenALPR training utility to manually find and mark license plates on photos.
+.SH SYNOPSIS
+.B plates
+.br
+.SH DESCRIPTION
+OpenALPR is an open source Automatic License Plate Recognition library written
+in C++ with bindings in C#, Java, Node.js, Go, and Python. The library analyzes
+images and video streams to identify license plates. The output is the text
+representation of any license plate characters.
+
+The purpose of this applications is to train OpenALPR to find license plates.
+
+See https://github.com/openalpr/openalpr for more.
+.SH AUTHOR
+plates was written by Dmitry Baryshev.
+.PP
+This manual page was written by Dmitry Baryshev ,
+for the Debian project (but may be used by others).
diff --git a/options.cpp b/options.cpp
new file mode 100644
index 0000000..3d5fcd8
--- /dev/null
+++ b/options.cpp
@@ -0,0 +1,29 @@
+#include "settings.h"
+#include "options.h"
+#include "ui_options.h"
+
+Options::Options(QWidget *parent)
+ : QDialog(parent)
+ , ui(new Ui::Options)
+{
+ ui->setupUi(this);
+
+ // load settings
+ ui->checkSkipTagged->setChecked(SETTINGS_GET_BOOL(SETTING_SKIP_TAGGED));
+ ui->checkMove->setChecked(SETTINGS_GET_BOOL(SETTING_MOVE_UNTAGGED));
+ ui->checkTooltips->setChecked(SETTINGS_GET_BOOL(SETTING_TOOLTIPS));
+}
+
+Options::~Options()
+{
+ delete ui;
+}
+
+void Options::slotAccept()
+{
+ SETTINGS_SET_BOOL(SETTING_SKIP_TAGGED, ui->checkSkipTagged->isChecked(), Settings::NoSync);
+ SETTINGS_SET_BOOL(SETTING_MOVE_UNTAGGED, ui->checkMove->isChecked(), Settings::NoSync);
+ SETTINGS_SET_BOOL(SETTING_TOOLTIPS, ui->checkTooltips->isChecked());
+
+ accept();
+}
diff --git a/options.h b/options.h
new file mode 100644
index 0000000..f84eac1
--- /dev/null
+++ b/options.h
@@ -0,0 +1,26 @@
+#ifndef OPTIONS_H
+#define OPTIONS_H
+
+#include
+
+namespace Ui
+{
+ class Options;
+}
+
+class Options : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit Options(QWidget *parent = 0);
+ ~Options();
+
+private slots:
+ void slotAccept();
+
+private:
+ Ui::Options *ui;
+};
+
+#endif // OPTIONS_H
diff --git a/options.ui b/options.ui
new file mode 100644
index 0000000..fcab210
--- /dev/null
+++ b/options.ui
@@ -0,0 +1,101 @@
+
+
+ Options
+
+
+
+ 0
+ 0
+ 316
+ 116
+
+
+
+ Preferences
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 4
+
+
+
+
+ -
+
+
+ Skip tagged images
+
+
+
+ -
+
+
+ Move images without plates into "negatives" subdirectory
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+ -
+
+
+ Show tooltips with a license plate information
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Options
+ slotAccept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Options
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
+ slotAccept()
+
+
diff --git a/platefile.cpp b/platefile.cpp
new file mode 100644
index 0000000..ce401ca
--- /dev/null
+++ b/platefile.cpp
@@ -0,0 +1,204 @@
+#include
+
+#include "platefile.h"
+
+PlateFile::PlateFile(const QString &platePathTemplate, int index)
+ : m_plateFileTemplate(platePathTemplate)
+ , m_plateFile(platePathTemplate + QString("-%1.yaml").arg(index))
+ , m_imageWidth(0)
+ , m_imageHeight(0)
+ , m_plateInverted(false)
+{
+ // very simple YAML parser
+ QFile file(m_plateFile);
+
+ if(!file.open(QFile::ReadOnly))
+ return;
+
+ QByteArray line;
+
+ while(!file.atEnd())
+ {
+ line = file.readLine();
+
+ // nothing to parse
+ if(line.isEmpty())
+ continue;
+
+ QTextStream ts(&line, QIODevice::ReadOnly);
+ ts.setCodec("UTF-8");
+
+ QByteArray key;
+
+ ts >> key;
+
+ if(key == "image_file:")
+ {} // nothing, we set image_file explicitely when saving
+ else if(key == "image_width:")
+ ts >> m_imageWidth;
+ else if(key == "image_height:")
+ ts >> m_imageHeight;
+ else if(key == "region_code_gt:")
+ ts >> m_regionCode;
+ else if(key == "plate_number_gt:")
+ {
+ QString text;
+ ts >> text;
+
+ qDebug() << text;
+
+ // literal multiline
+ if(text == "|")
+ {
+ while(!file.atEnd())
+ {
+ line = file.readLine().trimmed();
+
+ if(line.isEmpty())
+ break;
+
+ m_plateNumber.append(QString::fromUtf8(line) + '\n');
+ }
+
+ // cut the last \n
+ m_plateNumber.chop(1);
+ }
+ else
+ m_plateNumber = text;
+ }
+ else if(key == "plate_corners_gt:")
+ {
+ int x, y;
+ QPolygon polygon;
+
+ while(true)
+ {
+ ts.skipWhiteSpace();
+
+ if(ts.atEnd())
+ break;
+
+ ts >> x >> y;
+ polygon.append(QPoint(x, y));
+ }
+
+ if(polygon.size() == 4)
+ m_plateCorners = polygon;
+ else
+ qWarning("Polygon in file '%s' has only %d point(s)", qPrintable(m_plateFile), polygon.size());
+ }
+ else if(key == "plate_inverted_gt:")
+ {
+ QString boolString;
+ ts >> boolString;
+ m_plateInverted = (boolString == "true");
+ }
+ }
+}
+
+bool PlateFile::writeToFile(int index, QString *error) const
+{
+ // very simple YAML writer
+ QFile file(m_plateFileTemplate + QString("-%1.yaml").arg(index));
+
+ if(!file.open(QFile::WriteOnly | QFile::Truncate))
+ {
+ if(error)
+ *error = file.errorString();
+
+ return false;
+ }
+
+ QTextStream ts(&file);
+
+ ts.setCodec("UTF-8");
+
+ QStringList cornersStringList;
+
+ cornersStringList.reserve(m_plateCorners.size() * 2);
+
+ foreach(const QPoint &point, m_plateCorners)
+ {
+ cornersStringList.append(QString::number(point.x()));
+ cornersStringList.append(QString::number(point.y()));
+ }
+
+ ts
+ << "image_file: " << m_imageFile << endl
+ << "image_width: " << m_imageWidth << endl
+ << "image_height: " << m_imageHeight << endl;
+
+ // don't save an empty region code
+ if(!m_regionCode.isEmpty())
+ ts << "region_code_gt: " << m_regionCode << endl;
+
+ // write with new lines or not?
+ if(m_plateNumber.contains('\n'))
+ {
+ /*
+ * literal style:
+ *
+ * plate_number_gt: |
+ * plate number
+ * with
+ * new lines
+ *
+ */
+ const QStringList &lines = m_plateNumber.split('\n');
+
+ ts << "plate_number_gt: " << "|" << endl;
+
+ // write line-by-line and ignore empty lines
+ foreach(const QString &line, lines)
+ {
+ ts << ' ' << line << endl;
+ }
+
+ ts << endl;
+ }
+ else
+ ts << "plate_number_gt: " << m_plateNumber << endl;
+
+ ts
+ << "plate_corners_gt: " << cornersStringList.join(" ") << endl
+ << "plate_inverted_gt: " << (m_plateInverted ? "true" : "false") << endl
+ ;
+
+ ts.flush();
+
+ if(ts.status() != QTextStream::Ok)
+ {
+ if(error)
+ *error = QObject::tr("Cannot write into a file");
+
+ return false;
+ }
+
+ return true;
+}
+
+bool PlateFile::exists()
+{
+ return QFileInfo(m_plateFile).exists();
+}
+
+PlateFileList PlateFile::fromImageFile(const QString &fileTemplate)
+{
+ PlateFileList plateList;
+
+ int index = 0;
+
+ // find associated car-nnn.yaml files
+ while(true)
+ {
+ PlateFile plateFile(fileTemplate, index++);
+
+ if(!plateFile.exists())
+ break;
+
+ if(plateFile.isValid())
+ plateList.append(plateFile);
+ }
+
+ return plateList;
+}
diff --git a/platefile.h b/platefile.h
new file mode 100644
index 0000000..4bda381
--- /dev/null
+++ b/platefile.h
@@ -0,0 +1,164 @@
+#ifndef PLATEFILE_H
+#define PLATEFILE_H
+
+#include
+#include
+#include
+
+class QTextStream;
+class QFileInfo;
+
+class PlateFile;
+using PlateFileList = QList;
+
+class PlateFile
+{
+public:
+ PlateFile(const QString &platePathTemplate, int index);
+
+ /*
+ * Overwrite a plate file with index 'index'
+ */
+ bool writeToFile(int index, QString *error = nullptr) const;
+
+ /*
+ * Plate file template like "/data/car" from "/data/car.jpg"
+ */
+ QString plateFileTemplate() const;
+
+ /*
+ * Check is the corresponding plate file exists
+ */
+ bool exists();
+
+ /*
+ * PlateFile is valid when it has
+ * - an image path set
+ * - valid image dimensions
+ * - valid plate corners
+ */
+ bool isValid() const;
+
+ /*
+ * Some getters/setters
+ */
+ void setImageFile(const QString &imageFile);
+ void setImageWidth(int imageWidth);
+ void setImageHeight(int imageHeight);
+
+ QString regionCode() const;
+ void setRegionCode(const QString ®ionCode);
+
+ QString plateNumber() const;
+ void setPlateNumber(const QString &plateNumber);
+
+ QPolygon plateCorners() const;
+ void setPlateCorners(const QPolygon &plateCorners);
+
+ bool plateInverted() const;
+ void setPlateInverted(bool plateInverted);
+
+ /*
+ * Get a list of associated plates for the specified image file
+ *
+ * If an image file is "car.jpg", it will look for
+ *
+ * car-0.yaml
+ * car-1.yaml
+ * etc.
+ */
+ static PlateFileList fromImageFile(const QString &fileTemplate);
+
+private:
+ QString m_plateFileTemplate;
+ QString m_plateFile;
+ QString m_imageFile;
+ int m_imageWidth;
+ int m_imageHeight;
+ QString m_regionCode;
+ QString m_plateNumber;
+ QPolygon m_plateCorners;
+ bool m_plateInverted;
+};
+
+inline
+QString PlateFile::plateFileTemplate() const
+{
+ return m_plateFileTemplate;
+}
+
+inline
+bool PlateFile::isValid() const
+{
+ return m_imageWidth > 0
+ && m_imageHeight > 0
+ && !m_plateCorners.isEmpty();
+}
+
+inline
+void PlateFile::setImageFile(const QString &imageFile)
+{
+ m_imageFile = imageFile;
+}
+
+inline
+void PlateFile::setImageWidth(int imageWidth)
+{
+ m_imageWidth = imageWidth;
+}
+
+inline
+void PlateFile::setImageHeight(int imageHeight)
+{
+ m_imageHeight = imageHeight;
+}
+
+inline
+QString PlateFile::regionCode() const
+{
+ return m_regionCode;
+}
+
+inline
+void PlateFile::setRegionCode(const QString ®ionCode)
+{
+ m_regionCode = regionCode;
+}
+
+inline
+QString PlateFile::plateNumber() const
+{
+ return m_plateNumber;
+}
+
+inline
+void PlateFile::setPlateNumber(const QString &plateNumber)
+{
+ m_plateNumber = plateNumber;
+}
+
+inline
+QPolygon PlateFile::plateCorners() const
+{
+ return m_plateCorners;
+}
+
+inline
+void PlateFile::setPlateCorners(const QPolygon &plateCorners)
+{
+ m_plateCorners = plateCorners;
+}
+
+inline
+bool PlateFile::plateInverted() const
+{
+ return m_plateInverted;
+}
+
+inline
+void PlateFile::setPlateInverted(bool plateInverted)
+{
+ m_plateInverted = plateInverted;
+}
+
+#endif // PLATEFILE_H
diff --git a/plates.desktop b/plates.desktop
new file mode 100644
index 0000000..2548a63
--- /dev/null
+++ b/plates.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Encoding=UTF-8
+Type=Application
+Categories=Qt;Education;
+Exec=plates
+Icon=plates
+Terminal=false
+Name=OpenALPR training utility
+Comment=OpenALPR training utility
diff --git a/plates.iss b/plates.iss
new file mode 100644
index 0000000..d8aaabc
--- /dev/null
+++ b/plates.iss
@@ -0,0 +1,105 @@
+#define MyAppName "OpenALPR Training Utility"
+#define MyAppPublisher "OpenALPR"
+#define MyAppVersion "1.0.0"
+#define MyAppTemplate "plates"
+#define MyAppExecutable MyAppTemplate + ".exe"
+
+[Setup]
+AppId={{2B5FE8FF-0856-4433-9FBD-57318237B5D4}
+AppName={#MyAppName}
+AppVersion={#MyAppVersion}
+AppPublisher={#MyAppPublisher}
+DefaultDirName={pf}\{#MyAppName}
+DefaultGroupName={#MyAppName}
+OutputDir=.
+OutputBaseFilename={#MyAppTemplate}-setup-{#MyAppVersion}
+SetupIconFile={#MyAppTemplate}.ico
+Compression=lzma
+SolidCompression=yes
+UninstallDisplayName={#MyAppName}
+UninstallDisplayIcon={app}\{#MyAppExecutable}
+MinVersion=0,5.1
+
+[Languages]
+Name: "AAAenglish"; MessagesFile: "compiler:Default.isl"
+Name: "BrazilianPortuguesexisl"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl"
+Name: "Catalanxisl"; MessagesFile: "compiler:Languages\Catalan.isl"
+Name: "Corsicanxisl"; MessagesFile: "compiler:Languages\Corsican.isl"
+Name: "Czechxisl"; MessagesFile: "compiler:Languages\Czech.isl"
+Name: "Danishxisl"; MessagesFile: "compiler:Languages\Danish.isl"
+Name: "Dutchxisl"; MessagesFile: "compiler:Languages\Dutch.isl"
+Name: "Finnishxisl"; MessagesFile: "compiler:Languages\Finnish.isl"
+Name: "Frenchxisl"; MessagesFile: "compiler:Languages\French.isl"
+Name: "Germanxisl"; MessagesFile: "compiler:Languages\German.isl"
+Name: "Greekxisl"; MessagesFile: "compiler:Languages\Greek.isl"
+Name: "Hebrewxisl"; MessagesFile: "compiler:Languages\Hebrew.isl"
+Name: "Hungarianxisl"; MessagesFile: "compiler:Languages\Hungarian.isl"
+Name: "Italianxisl"; MessagesFile: "compiler:Languages\Italian.isl"
+Name: "Japanesexisl"; MessagesFile: "compiler:Languages\Japanese.isl"
+Name: "Norwegianxisl"; MessagesFile: "compiler:Languages\Norwegian.isl"
+Name: "Polishxisl"; MessagesFile: "compiler:Languages\Polish.isl"
+Name: "Portuguesexisl"; MessagesFile: "compiler:Languages\Portuguese.isl"
+Name: "Russianxisl"; MessagesFile: "compiler:Languages\Russian.isl"
+Name: "ScottishGaelicxisl"; MessagesFile: "compiler:Languages\ScottishGaelic.isl"
+Name: "SerbianCyrillicxisl"; MessagesFile: "compiler:Languages\SerbianCyrillic.isl"
+Name: "SerbianLatinxisl"; MessagesFile: "compiler:Languages\SerbianLatin.isl"
+Name: "Slovenianxisl"; MessagesFile: "compiler:Languages\Slovenian.isl"
+Name: "Spanishxisl"; MessagesFile: "compiler:Languages\Spanish.isl"
+Name: "Turkishxisl"; MessagesFile: "compiler:Languages\Turkish.isl"
+Name: "Ukrainianxisl"; MessagesFile: "compiler:Languages\Ukrainian.isl"
+
+[Files]
+Source: "COPYING-runtime.txt"; DestDir: "{app}"; Flags: ignoreversion
+Source: "COPYING-winpthreads.txt"; DestDir: "{app}"; Flags: ignoreversion
+Source: "libgcc_s_dw2-1.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "libstdc++-6.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "libwinpthread-1.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "Qt5Core.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "Qt5Gui.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "Qt5Widgets.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "imageformats/qico.dll"; DestDir: "{app}/imageformats"; Flags: ignoreversion
+Source: "imageformats/qjpeg.dll"; DestDir: "{app}/imageformats"; Flags: ignoreversion
+Source: "platforms/qwindows.dll"; DestDir: "{app}/platforms"; Flags: ignoreversion
+Source: "{#MyAppExecutable}"; DestDir: "{app}"; Flags: ignoreversion
+
+[Tasks]
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
+Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; OnlyBelowVersion: 0,6.1
+
+[Run]
+Filename: {app}\{#MyAppExecutable}; Description: {cm:LaunchProgram,{#MyAppName}}; Flags: nowait postinstall skipifsilent
+
+[Icons]
+Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExecutable}"
+Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
+Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExecutable}"; Tasks: desktopicon
+Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExecutable}"; Tasks: quicklaunchicon
+
+[Code]
+procedure CurStepChanged(CurStep: TSetupStep);
+var
+ResultCode: Integer;
+Uninstall: String;
+UninstallQuery : String;
+begin
+UninstallQuery := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1');
+ if (CurStep = ssInstall) then begin
+ if RegQueryStringValue(HKLM, UninstallQuery, 'UninstallString', Uninstall)
+ or RegQueryStringValue(HKCU, UninstallQuery, 'UninstallString', Uninstall) then begin
+ Uninstall := RemoveQuotes(Uninstall)
+ if (FileExists(Uninstall)) AND (not Exec(RemoveQuotes(Uninstall), '/VERYSILENT', '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode)) then begin
+ MsgBox(SysErrorMessage(ResultCode), mbCriticalError, MB_OK);
+ Abort();
+ end;
+ end;
+ end;
+end;
+
+procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
+ var ErrorCode: Integer;
+begin
+ if (CurUninstallStep = usUninstall) then begin
+ ShellExec('open','taskkill.exe','/f /im {#MyAppExecutable}','',SW_HIDE,ewNoWait,ErrorCode);
+ ShellExec('open','tskill.exe',' {#MyAppName}','',SW_HIDE,ewNoWait,ErrorCode);
+ end;
+end;
diff --git a/plates.pro b/plates.pro
new file mode 100644
index 0000000..2dc1c9a
--- /dev/null
+++ b/plates.pro
@@ -0,0 +1,93 @@
+TEMPLATE = app
+TARGET = plates
+TARGET_HUMAN = OpenALPR Training Utility
+
+QT += core gui widgets
+CONFIG += c++11
+
+DEFINES += QT_DEPRECATED_WARNINGS
+DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x050000
+
+VERSION_MAJOR=1
+VERSION_MINOR=0
+VERSION_PATCH=0
+
+VERSION=$$sprintf("%1.%2.%3", $$VERSION_MAJOR, $$VERSION_MINOR, $$VERSION_PATCH)
+
+DEFINES += VERSION_MAJOR=$$VERSION_MAJOR
+DEFINES += VERSION_MINOR=$$VERSION_MINOR
+DEFINES += VERSION_PATCH=$$VERSION_PATCH
+DEFINES += VERSION_STRING=$$sprintf("\"\\\"%1\\\"\"", $$VERSION)
+
+DEFINES += TARGET=$$TARGET
+DEFINES += TARGET_STRING=$$sprintf("\"\\\"%1\\\"\"", $$TARGET)
+UP=$$sprintf("%1", $$TARGET)
+DEFINES += TARGET_UPPER_STRING=$$sprintf("\"\\\"%1\\\"\"", $$upper($$UP))
+DEFINES += TARGET_HUMAN_STRING=$$sprintf("\"\\\"%1\\\"\"", $$TARGET_HUMAN)
+
+win32 {
+ RC_FILE = $${TARGET}.rc
+} else:macx {
+ QMAKE_INFO_PLIST = Info.plist
+ ICON = images/$${TARGET}.icns
+}
+
+########################################
+
+INCLUDEPATH += . qprogressindicator
+DEPENDPATH += . qprogressindicator
+
+SOURCES += main.cpp \
+ mainwindow.cpp \
+ settings.cpp \
+ utils.cpp \
+ qprogressindicator/QProgressIndicator.cpp \
+ directoryloader.cpp \
+ about.cpp \
+ imagefile.cpp \
+ options.cpp \
+ platefile.cpp \
+ threadedimageloader.cpp \
+ imageview.cpp \
+ squeezedlabel.cpp \
+ polygons.cpp \
+ selection.cpp \
+ imageviewerbase.cpp \
+ imageviewerobserver.cpp \
+ plateselector.cpp \
+ imageviewerplateselector.cpp \
+ dot.cpp
+
+HEADERS += mainwindow.h \
+ settings.h \
+ utils.h \
+ qprogressindicator/QProgressIndicator.h \
+ directoryloader.h \
+ about.h \
+ imagefile.h \
+ options.h \
+ platefile.h \
+ threadedimageloader.h \
+ imageview.h \
+ squeezedlabel.h \
+ polygons.h \
+ selection.h \
+ imageviewerbase.h \
+ imageviewerobserver.h \
+ plateselector.h \
+ imageviewerplateselector.h \
+ dot.h
+
+FORMS += mainwindow.ui \
+ directoryloader.ui \
+ about.ui \
+ options.ui \
+ imageview.ui \
+ plateselector.ui
+
+RESOURCES += \
+ $${TARGET}.qrc
+
+DISTFILES += \
+ other/spec.txt \
+ README.txt
diff --git a/plates.qrc b/plates.qrc
new file mode 100644
index 0000000..3691c7a
--- /dev/null
+++ b/plates.qrc
@@ -0,0 +1,5 @@
+
+
+ images/plates.ico
+
+
diff --git a/plates.rc b/plates.rc
new file mode 100644
index 0000000..f9eed95
--- /dev/null
+++ b/plates.rc
@@ -0,0 +1,42 @@
+#include
+#include
+#include
+
+#define stringify(v1) #v1
+#define quote(v1) stringify(v1)
+
+#define NVERSION VERSION_MAJOR.VERSION_MINOR.VERSION_PATCH
+
+STRINGTABLE LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
+BEGIN
+ 1 quote(TARGET_UPPER)quote(TARGET)"\0"
+END
+
+IDI_ICON1 ICON DISCARDABLE "images/plates.ico"
+
+1 VERSIONINFO
+ FILEVERSION VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, 0
+ PRODUCTVERSION VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, 0
+ FILEOS VOS_UNKNOWN
+ FILETYPE VFT_APP
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "Comments", quote(TARGET) ""
+ VALUE "CompanyName", "OpenALPR"
+ VALUE "FileDescription", quote(TARGET)
+ VALUE "FileVersion", quote(NVERSION)
+ VALUE "InternalName", quote(TARGET)
+ VALUE "LegalCopyright", "(C) 2017 OpenALPR"
+ VALUE "ProductName", quote(TARGET)
+ VALUE "ProductVersion", quote(NVERSION)
+ VALUE "Builder", "OpenALPR"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200
+ END
+END
diff --git a/plateselector.cpp b/plateselector.cpp
new file mode 100644
index 0000000..bfab307
--- /dev/null
+++ b/plateselector.cpp
@@ -0,0 +1,171 @@
+#include
+#include
+
+#include "plateselector.h"
+#include "platefile.h"
+#include "utils.h"
+#include "ui_plateselector.h"
+
+PlateSelector::PlateSelector(const QImage &image, QWidget *parent)
+ : QDialog(parent)
+ , ui(new Ui::PlateSelector)
+{
+ ui->setupUi(this);
+
+ setWindowFlags(windowFlags() | Qt::CustomizeWindowHint | Qt::WindowMaximizeButtonHint);
+
+ ui->imageViewerPlateSelector->setImage(image);
+
+ ui->plainTextPlateNumber->ensurePolished();
+ ui->plainTextPlateNumber->setFixedHeight(
+ (ui->plainTextPlateNumber->document()->documentMargin()
+ + QFontMetrics(ui->plainTextPlateNumber->font()).lineSpacing()) * 2);
+
+ ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false);
+ ui->buttonBox->button(QDialogButtonBox::Reset)->setText(tr("Reset corners"));
+ ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(tr("Delete && Save"));
+
+ // shortcuts
+ ui->buttonBox->button(QDialogButtonBox::Reset)->setShortcut(Qt::CTRL+Qt::Key_Q);
+ ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setShortcut(Qt::CTRL+Qt::Key_D);
+ ui->buttonBox->button(QDialogButtonBox::Save)->setShortcut(Qt::CTRL+Qt::Key_S);
+ ui->buttonBox->button(QDialogButtonBox::Help)->setShortcut(QKeySequence::HelpContents);
+
+ m_shortCutPlateNumber = new QShortcut(Qt::CTRL+Qt::Key_N, ui->plainTextPlateNumber, SLOT(setFocus()));
+ m_shortCutPlateRegion = new QShortcut(Qt::CTRL+Qt::Key_R, ui->linePlateRegion, SLOT(setFocus()));
+ m_shortCutLightOnDark = new QShortcut(Qt::CTRL+Qt::Key_L, ui->checkLightOnDark, SLOT(toggle()));
+
+ // set tooltips
+ Utils::setShortcutTooltips(QList()
+ << ui->buttonBox->button(QDialogButtonBox::Reset)
+ << ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)
+ << ui->buttonBox->button(QDialogButtonBox::Save)
+ << ui->buttonBox->button(QDialogButtonBox::Help));
+
+ connect(ui->imageViewerPlateSelector, SIGNAL(dotsChanged(int)), this, SLOT(slotCheckData()));
+}
+
+PlateSelector::~PlateSelector()
+{
+ delete ui;
+}
+
+void PlateSelector::setPlateFile(const PlateFile &plateFile)
+{
+ qDebug() << "Existing polygon:" << plateFile.plateCorners();
+
+ foreach(const QPoint &point, plateFile.plateCorners())
+ {
+ ui->imageViewerPlateSelector->addDotInImageCoordinates(point - m_selection.topLeft());
+ }
+
+ ui->plainTextPlateNumber->setPlainText(plateFile.plateNumber());
+ ui->linePlateRegion->setText(plateFile.regionCode());
+ ui->checkLightOnDark->setChecked(plateFile.plateInverted());
+}
+
+QString PlateSelector::plateNumber() const
+{
+ QString result;
+ const QStringList plainLines = ui->plainTextPlateNumber->toPlainText().trimmed().split('\n', QString::SkipEmptyParts);
+
+ foreach(QString plainLine, plainLines)
+ {
+ plainLine = plainLine.trimmed();
+
+ if(!plainLine.isEmpty())
+ result += plainLine + '\n';
+ }
+
+ // cut the last '\n'
+ result.chop(1);
+
+ return result;
+}
+
+QString PlateSelector::plateRegion() const
+{
+ return ui->linePlateRegion->text().trimmed();
+}
+
+bool PlateSelector::lightOnDark() const
+{
+ return ui->checkLightOnDark->isChecked();
+}
+
+void PlateSelector::slotCheckData()
+{
+ ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(
+ !plateNumber().isEmpty()
+ && !ui->imageViewerPlateSelector->selectedPolygon().isEmpty()
+ );
+}
+
+void PlateSelector::slotSave()
+{
+ m_selectedPolygon = ui->imageViewerPlateSelector->selectedPolygon();
+
+ for(int i = 0;i < m_selectedPolygon.size();i++)
+ {
+ m_selectedPolygon[i] += m_selection.topLeft();
+ }
+
+ qDebug() << "Selected polygon:" << m_selectedPolygon;
+
+ accept();
+}
+
+void PlateSelector::slotHelp()
+{
+ qDebug("Help");
+
+ Utils::help(tr("Click the four corners of the license plate. "
+ "The lines should align with the edge of the license plate.
"
+ "Required fields:"
+ ""
+ "- Plate corners - the four corners of the license plate in the image"
+ "
- Plate number - the plate number of the license plate"
+ "
"
+ "Optional:"
+ ""
+ "- Plate region - the country/state of issuance (e.g. California)"
+ "
- Light-on-Dark letters - the plate background is darker than the letters (e.g. white characters on a dark blue background)"
+ "
"
+ "Hotkeys:"
+ ""
+ "- Tab - Move to the next field"
+ "
- %1 - Jump to the plate number field"
+ "
- %2 - Jump to the plate region field"
+ "
- %3 - Toggle Light-on-Dark letters"
+ "
- %4 - Reset corners"
+ "
- %5 - Delete the corners and save"
+ "
- %6 - Save"
+ "
- Escape - Cancel"
+ "
- %7 - Help"
+ "
")
+ .arg(m_shortCutPlateNumber->key().toString())
+ .arg(m_shortCutPlateRegion->key().toString())
+ .arg(m_shortCutLightOnDark->key().toString())
+ .arg(ui->buttonBox->button(QDialogButtonBox::Reset)->shortcut().toString())
+ .arg(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->shortcut().toString())
+ .arg(ui->buttonBox->button(QDialogButtonBox::Save)->shortcut().toString())
+ .arg(ui->buttonBox->button(QDialogButtonBox::Help)->shortcut().toString())
+ , this);
+}
+
+void PlateSelector::slotClicked(QAbstractButton *button)
+{
+ // reset
+ if(ui->buttonBox->button(QDialogButtonBox::Reset) == button)
+ {
+ qDebug("Reset corners");
+ ui->imageViewerPlateSelector->clearDots();
+ }
+ // delete
+ else if(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults) == button)
+ {
+ qDebug("Delete plate");
+ ui->imageViewerPlateSelector->clearDots();
+ accept();
+ }
+}
diff --git a/plateselector.h b/plateselector.h
new file mode 100644
index 0000000..412fc8e
--- /dev/null
+++ b/plateselector.h
@@ -0,0 +1,75 @@
+#ifndef PLATESELECTOR_H
+#define PLATESELECTOR_H
+
+#include
+#include
+
+class QAbstractButton;
+class QShortcut;
+
+class PlateFile;
+
+namespace Ui
+{
+ class PlateSelector;
+}
+
+/*
+ * Dialog to select or edit a license plate with four points
+ */
+class PlateSelector : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit PlateSelector(const QImage &image, QWidget *parent = 0);
+ ~PlateSelector();
+
+ /*
+ * Set an existing plate to edit
+ */
+ void setPlateFile(const PlateFile &plateFile);
+
+ /*
+ * Selection rectangle in the original (full) image. This rectangle is used to re-calculate
+ * correct relative dot coordinates and pass them into addDotInImageCoordinates() method
+ */
+ void setSelectionRect(const QRect &rect);
+
+ /*
+ * Getters
+ */
+ QString plateNumber() const;
+ QString plateRegion() const;
+ bool lightOnDark() const;
+
+ QPolygon selectedPolygon() const;
+
+private slots:
+ void slotCheckData();
+ void slotSave();
+ void slotHelp();
+ void slotClicked(QAbstractButton *button);
+
+private:
+ Ui::PlateSelector *ui;
+ QRect m_selection;
+ QPolygon m_selectedPolygon;
+ QShortcut *m_shortCutPlateNumber;
+ QShortcut *m_shortCutPlateRegion;
+ QShortcut *m_shortCutLightOnDark;
+};
+
+inline
+void PlateSelector::setSelectionRect(const QRect &rect)
+{
+ m_selection = rect;
+}
+
+inline
+QPolygon PlateSelector::selectedPolygon() const
+{
+ return m_selectedPolygon;
+}
+
+#endif // PLATESELECTOR_H
diff --git a/plateselector.ui b/plateselector.ui
new file mode 100644
index 0000000..9b02b8a
--- /dev/null
+++ b/plateselector.ui
@@ -0,0 +1,213 @@
+
+
+ PlateSelector
+
+
+
+ 0
+ 0
+ 700
+ 500
+
+
+
+ License plate selection
+
+
+
+ 10
+
+ -
+
+
+ 6
+
+
-
+
+
+ Plate number:
+
+
+
+ -
+
+
+ -
+
+
+ Plate region:
+
+
+
+ -
+
+
+ 64
+
+
+
+ -
+
+
+ Light-on-Dark letters
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 0
+ 10
+
+
+
+
+
+
+ -
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Reset|QDialogButtonBox::RestoreDefaults|QDialogButtonBox::Save
+
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ ImageViewerPlateSelector
+ QWidget
+ imageviewerplateselector.h
+ 1
+
+
+
+ plainTextPlateNumber
+ linePlateRegion
+ checkLightOnDark
+
+
+
+
+ buttonBox
+ rejected()
+ PlateSelector
+ reject()
+
+
+ 549
+ 490
+
+
+ 541
+ 440
+
+
+
+
+ buttonBox
+ accepted()
+ PlateSelector
+ slotSave()
+
+
+ 521
+ 490
+
+
+ 503
+ 431
+
+
+
+
+ buttonBox
+ helpRequested()
+ PlateSelector
+ slotHelp()
+
+
+ 383
+ 490
+
+
+ 366
+ 449
+
+
+
+
+ buttonBox
+ clicked(QAbstractButton*)
+ PlateSelector
+ slotClicked(QAbstractButton*)
+
+
+ 202
+ 490
+
+
+ 198
+ 440
+
+
+
+
+ plainTextPlateNumber
+ textChanged()
+ PlateSelector
+ slotCheckData()
+
+
+ 243
+ 387
+
+
+ 252
+ 519
+
+
+
+
+
+ slotSave()
+ slotCheckData()
+ slotHelp()
+ slotClicked(QAbstractButton*)
+
+
diff --git a/polygons.cpp b/polygons.cpp
new file mode 100644
index 0000000..647cd3c
--- /dev/null
+++ b/polygons.cpp
@@ -0,0 +1,48 @@
+#include
+
+#include "polygons.h"
+#include "utils.h"
+
+Polygons::Polygons()
+{}
+
+void Polygons::setZoom(double zoom)
+{
+ m_scaledPolygons.clear();
+ m_scaledPolygons.reserve(m_polygons.size());
+
+ foreach(QPolygon polygon, m_polygons)
+ {
+ for(int i = 0;i < polygon.size();i++)
+ {
+ polygon.setPoint(i, polygon.point(i) * zoom);
+ }
+
+ m_scaledPolygons.append(polygon);
+ }
+}
+
+void Polygons::draw(QPainter *painter)
+{
+ if(!painter)
+ return;
+
+ painter->setPen(Utils::polygonPaintingPen());
+
+ foreach(const QPolygon &polygon, m_scaledPolygons)
+ {
+ painter->drawPolygon(polygon);
+ }
+}
+
+Polygons Polygons::fromPlates(const PlateFileList &plateFileList)
+{
+ Polygons polygons;
+
+ foreach(const PlateFile &plateFile, plateFileList)
+ {
+ polygons.append(plateFile.plateCorners());
+ }
+
+ return polygons;
+}
diff --git a/polygons.h b/polygons.h
new file mode 100644
index 0000000..09066da
--- /dev/null
+++ b/polygons.h
@@ -0,0 +1,57 @@
+#ifndef POLYGONS_H
+#define POLYGONS_H
+
+#include
+#include
+
+#include "platefile.h"
+
+class QPainter;
+
+/*
+ * Class to hold static license plates polygons and draw them
+ */
+class Polygons
+{
+public:
+ Polygons();
+
+ /*
+ * QList proxy methods
+ */
+ void append(const QPolygon &polygon);
+ void clear();
+
+ /*
+ * Scale the currently saved polygons
+ */
+ void setZoom(double zoom);
+
+ /*
+ * Draw currently saved polygons with the given painter
+ */
+ void draw(QPainter *painter);
+
+ /*
+ * Constructs a 'Polygons' object from saved license plates
+ */
+ static Polygons fromPlates(const PlateFileList &plateFileList);
+
+private:
+ QList m_polygons;
+ QList m_scaledPolygons;
+};
+
+inline
+void Polygons::append(const QPolygon &polygon)
+{
+ m_polygons.append(polygon);
+}
+
+inline
+void Polygons::clear()
+{
+ m_polygons.clear();
+}
+
+#endif // POLYGONS_H
diff --git a/qprogressindicator/QProgressIndicator.cpp b/qprogressindicator/QProgressIndicator.cpp
new file mode 100644
index 0000000..6b71044
--- /dev/null
+++ b/qprogressindicator/QProgressIndicator.cpp
@@ -0,0 +1,134 @@
+#include "QProgressIndicator.h"
+
+#include
+
+QProgressIndicator::QProgressIndicator(QWidget* parent)
+ : QWidget(parent),
+ m_angle(0),
+ m_timerId(-1),
+ m_delay(40),
+ m_displayedWhenStopped(false),
+ m_color(Qt::black)
+{
+ setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ setFocusPolicy(Qt::NoFocus);
+}
+
+bool QProgressIndicator::isAnimated () const
+{
+ return (m_timerId != -1);
+}
+
+void QProgressIndicator::setDisplayedWhenStopped(bool state)
+{
+ m_displayedWhenStopped = state;
+
+ update();
+}
+
+bool QProgressIndicator::isDisplayedWhenStopped() const
+{
+ return m_displayedWhenStopped;
+}
+
+void QProgressIndicator::startAnimation()
+{
+ m_angle = 0;
+
+ if (m_timerId == -1)
+ m_timerId = startTimer(m_delay);
+}
+
+void QProgressIndicator::stopAnimation()
+{
+ if (m_timerId != -1)
+ killTimer(m_timerId);
+
+ m_timerId = -1;
+
+ update();
+}
+
+void QProgressIndicator::setAnimationDelay(int delay)
+{
+ if (m_timerId != -1)
+ killTimer(m_timerId);
+
+ m_delay = delay;
+
+ if (m_timerId != -1)
+ m_timerId = startTimer(m_delay);
+}
+
+void QProgressIndicator::setColor(const QColor & color)
+{
+ m_color = color;
+
+ update();
+}
+
+QSize QProgressIndicator::sizeHint() const
+{
+ return QSize(20,20);
+}
+
+int QProgressIndicator::heightForWidth(int w) const
+{
+ return w;
+}
+
+void QProgressIndicator::timerEvent(QTimerEvent * /*event*/)
+{
+ m_angle = (m_angle+30)%360;
+
+ update();
+}
+
+void QProgressIndicator::paintEvent(QPaintEvent * /*event*/)
+{
+ if (!m_displayedWhenStopped && !isAnimated())
+ return;
+
+ int width = qMin(this->width(), this->height());
+
+ QPainter p(this);
+ p.setRenderHint(QPainter::Antialiasing);
+
+ int outerRadius = (width-1)*0.5;
+ int innerRadius = (width-1)*0.5*0.38;
+
+ int capsuleHeight = outerRadius - innerRadius;
+ int capsuleWidth = (width > 32 ) ? capsuleHeight *.23 : capsuleHeight *.35;
+ int capsuleRadius = capsuleWidth/2;
+
+/* CO-> */
+ if (isAnimated())
+ {
+ for (int i = 0; i < 12; ++i)
+ {
+ QColor color = m_color;
+ color.setAlphaF(1.0f - (i/12.0f));
+ p.setPen(Qt::NoPen);
+ p.setBrush(color);
+ p.save();
+ p.translate(rect().center());
+ p.rotate(m_angle - i * 30.0f);
+ p.drawRoundedRect(-capsuleWidth * 0.5, -(innerRadius+capsuleHeight), capsuleWidth, capsuleHeight, capsuleRadius, capsuleRadius);
+ p.restore();
+ }
+ }
+ else
+ for (int i = 0; i < 12; ++i)
+ {
+ QColor color = m_color;
+ color.setAlphaF(0.2F);
+ p.setPen(Qt::NoPen);
+ p.setBrush(color);
+ p.save();
+ p.translate(rect().center());
+ p.rotate(m_angle - i * 30.0f);
+ p.drawRoundedRect(-capsuleWidth * 0.5, -(innerRadius+capsuleHeight), capsuleWidth, capsuleHeight, capsuleRadius, capsuleRadius);
+ p.restore();
+ }
+/* <-CO */
+}
diff --git a/qprogressindicator/QProgressIndicator.h b/qprogressindicator/QProgressIndicator.h
new file mode 100644
index 0000000..700db81
--- /dev/null
+++ b/qprogressindicator/QProgressIndicator.h
@@ -0,0 +1,88 @@
+#ifndef QPROGRESSINDICATOR_H
+#define QPROGRESSINDICATOR_H
+
+#include
+#include
+
+/*!
+ \class QProgressIndicator
+ \brief The QProgressIndicator class lets an application display a progress indicator to show that a lengthy task is under way.
+
+ Progress indicators are indeterminate and do nothing more than spin to show that the application is busy.
+ \sa QProgressBar
+*/
+class QProgressIndicator : public QWidget
+{
+ Q_OBJECT
+ Q_PROPERTY(int delay READ animationDelay WRITE setAnimationDelay)
+ Q_PROPERTY(bool displayedWhenStopped READ isDisplayedWhenStopped WRITE setDisplayedWhenStopped)
+ Q_PROPERTY(QColor color READ color WRITE setColor)
+public:
+ QProgressIndicator(QWidget* parent = 0);
+
+ /*! Returns the delay between animation steps.
+ \return The number of milliseconds between animation steps. By default, the animation delay is set to 40 milliseconds.
+ \sa setAnimationDelay
+ */
+ int animationDelay() const { return m_delay; }
+
+ /*! Returns a Boolean value indicating whether the component is currently animated.
+ \return Animation state.
+ \sa startAnimation stopAnimation
+ */
+ bool isAnimated () const;
+
+ /*! Returns a Boolean value indicating whether the receiver shows itself even when it is not animating.
+ \return Return true if the progress indicator shows itself even when it is not animating. By default, it returns false.
+ \sa setDisplayedWhenStopped
+ */
+ bool isDisplayedWhenStopped() const;
+
+ /*! Returns the color of the component.
+ \sa setColor
+ */
+ const QColor & color() const { return m_color; }
+
+ virtual QSize sizeHint() const;
+ int heightForWidth(int w) const;
+public slots:
+ /*! Starts the spin animation.
+ \sa stopAnimation isAnimated
+ */
+ void startAnimation();
+
+ /*! Stops the spin animation.
+ \sa startAnimation isAnimated
+ */
+ void stopAnimation();
+
+ /*! Sets the delay between animation steps.
+ Setting the \a delay to a value larger than 40 slows the animation, while setting the \a delay to a smaller value speeds it up.
+ \param delay The delay, in milliseconds.
+ \sa animationDelay
+ */
+ void setAnimationDelay(int delay);
+
+ /*! Sets whether the component hides itself when it is not animating.
+ \param state The animation state. Set false to hide the progress indicator when it is not animating; otherwise true.
+ \sa isDisplayedWhenStopped
+ */
+ void setDisplayedWhenStopped(bool state);
+
+ /*! Sets the color of the components to the given color.
+ \sa color
+ */
+ void setColor(const QColor & color);
+
+protected:
+ virtual void timerEvent(QTimerEvent * event);
+ virtual void paintEvent(QPaintEvent * event);
+private:
+ int m_angle;
+ int m_timerId;
+ int m_delay;
+ bool m_displayedWhenStopped;
+ QColor m_color;
+};
+
+#endif // QPROGRESSINDICATOR_H
diff --git a/selection.cpp b/selection.cpp
new file mode 100644
index 0000000..180644a
--- /dev/null
+++ b/selection.cpp
@@ -0,0 +1,56 @@
+#include
+#include
+
+#include "selection.h"
+#include "utils.h"
+
+constexpr int SELECTION_MIN_WIDTH = 6;
+constexpr int SELECTION_MIN_HEIGHT = 6;
+
+Selection::Selection()
+ : m_hasSelection(false)
+{}
+
+bool Selection::selectionIsFine() const
+{
+ const QRect &selection = m_selection.normalized();
+ return m_hasSelection && selection.width() > SELECTION_MIN_WIDTH && selection.height() > SELECTION_MIN_HEIGHT;
+}
+
+void Selection::reset()
+{
+ m_hasSelection = false;
+ m_selection = QRect();
+}
+
+void Selection::start(const QPoint &pos)
+{
+ if(!m_referenceRect.contains(pos))
+ return;
+
+ m_hasSelection = true;
+ m_selection = QRect(pos, pos);
+
+ qDebug() << "Starting selection" << m_selection;
+}
+
+void Selection::move(const QPoint &pos)
+{
+ m_selection.setBottomRight(Utils::fitPoint(m_referenceRect, pos));
+}
+
+void Selection::draw(QPainter *painter)
+{
+ if(!painter)
+ return;
+
+ // selection must be normalized and checked against a minimal size
+ const QRect &selection = m_selection.normalized();
+
+ if(selectionIsFine())
+ {
+ painter->setCompositionMode(QPainter::RasterOp_SourceXorDestination);
+ painter->setPen(Qt::white);
+ painter->drawRect(selection);
+ }
+}
diff --git a/selection.h b/selection.h
new file mode 100644
index 0000000..2b66e3a
--- /dev/null
+++ b/selection.h
@@ -0,0 +1,92 @@
+#ifndef SELECTION_H
+#define SELECTION_H
+
+#include
+
+class QPainter;
+
+/*
+ * Rectangular selection painter
+ */
+class Selection
+{
+public:
+ Selection();
+
+ /*
+ * Actual selection rectangle
+ */
+ QRect selection() const;
+
+ /*
+ * In selection mode
+ */
+ bool hasSelection() const;
+
+ /*
+ * Has selection and it's enought large
+ */
+ bool selectionIsFine() const;
+
+ /*
+ * Reset to inital state
+ */
+ void reset();
+
+ /*
+ * Relative rectangle
+ */
+ void setReferenceRect(const QRect &rect);
+
+ /*
+ * Start painting a selection rectangle from the given position
+ */
+ void start(const QPoint &pos);
+
+ /*
+ * Move the selection rectangle
+ */
+ void move(const QPoint &pos);
+
+ /*
+ * Stop painting selection
+ */
+ void stop();
+
+ /*
+ * Paint selection using the specified painter
+ */
+ void draw(QPainter *painter);
+
+private:
+ QRect m_referenceRect;
+ QRect m_selection;
+ bool m_hasSelection;
+};
+
+inline
+QRect Selection::selection() const
+{
+ return m_selection;
+}
+
+inline
+bool Selection::hasSelection() const
+{
+ return m_hasSelection;
+}
+
+inline
+void Selection::setReferenceRect(const QRect &rect)
+{
+ m_referenceRect = rect;
+}
+
+inline
+void Selection::stop()
+{
+ m_hasSelection = false;
+ m_selection = m_selection.normalized();
+}
+
+#endif // SELECTION_H
diff --git a/settings.cpp b/settings.cpp
new file mode 100644
index 0000000..688ce85
--- /dev/null
+++ b/settings.cpp
@@ -0,0 +1,111 @@
+#include
+
+#include "settings.h"
+
+class SettingsPrivate
+{
+public:
+ QSettings *settings;
+ QMap translations;
+ QHash defaultValues;
+};
+
+/*******************************************************/
+
+Settings::Settings()
+{
+ d = new SettingsPrivate;
+
+ addDefaultValues();
+
+ d->settings = new QSettings(QSettings::IniFormat,
+ QSettings::UserScope,
+ QCoreApplication::organizationName(),
+ QCoreApplication::applicationName());
+
+ qDebug("Configuration file: %s", qPrintable(d->settings->fileName()));
+
+ d->settings->setFallbacksEnabled(false);
+}
+
+void Settings::addDefaultValues()
+{
+ QHash defaultValues;
+
+ defaultValues.insert(SETTING_SKIP_TAGGED, false);
+ defaultValues.insert(SETTING_MOVE_UNTAGGED, false);
+ defaultValues.insert(SETTING_TOOLTIPS, true);
+
+ addDefaultValues(defaultValues);
+}
+
+void Settings::addDefaultValues(const QHash &defaultValues)
+{
+ QHash::const_iterator itEnd = defaultValues.end();
+
+ for(QHash::const_iterator it = defaultValues.begin();it != itEnd;++it)
+ {
+ d->defaultValues.insert(it.key(), it.value());
+ }
+}
+
+void Settings::fillTranslations()
+{
+ d->translations.insert("en", "English");
+
+ // http://www.loc.gov/standards/iso639-2/php/code_list.php
+ // currently nothing to add
+}
+
+Settings::~Settings()
+{
+ delete d->settings;
+ delete d;
+}
+
+QVariant Settings::defaultValue(const QString &key) const
+{
+ return d->defaultValues.value(key);
+}
+
+void Settings::remove(const QString &key, Settings::SyncType sync)
+{
+ QSettings *s = settings();
+
+ s->beginGroup("settings");
+ s->remove(key);
+ s->endGroup();
+
+ if(sync == Sync)
+ s->sync();
+}
+
+void Settings::sync()
+{
+ d->settings->sync();
+}
+
+QMap Settings::translations()
+{
+ if(d->translations.isEmpty())
+ fillTranslations();
+
+ return d->translations;
+}
+
+Settings* Settings::instance()
+{
+ static Settings *inst = new Settings;
+
+ return inst;
+}
+
+QHash& Settings::defaultValues()
+{
+ return d->defaultValues;
+}
+
+QSettings *Settings::settings() const
+{
+ return d->settings;
+}
diff --git a/settings.h b/settings.h
new file mode 100644
index 0000000..17dc23b
--- /dev/null
+++ b/settings.h
@@ -0,0 +1,119 @@
+#ifndef SETTINGS_H
+#define SETTINGS_H
+
+#include
+
+#define SETTINGS_CONTAINS Settings::instance()->contains
+
+#define SETTINGS_GET_BOOL Settings::instance()->value
+#define SETTINGS_SET_BOOL Settings::instance()->setValue
+
+#define SETTINGS_GET_INT Settings::instance()->value
+#define SETTINGS_SET_INT Settings::instance()->setValue
+
+#define SETTINGS_GET_STRING Settings::instance()->value
+#define SETTINGS_SET_STRING Settings::instance()->setValue
+
+#define SETTINGS_REMOVE Settings::instance()->remove
+
+#define SETTING_LAST_DIRECTORY "last-directory"
+#define SETTING_SKIP_TAGGED "skip-tagged"
+#define SETTING_MOVE_UNTAGGED "move-untagged"
+#define SETTING_TOOLTIPS "tooltips"
+
+class SettingsPrivate;
+
+/*
+ * System settings
+ */
+class Settings
+{
+public:
+ static Settings* instance();
+
+ ~Settings();
+
+ enum SyncType { NoSync, Sync };
+
+ template
+ T value(const QString &key);
+
+ template
+ T value(const QString &key, const T &def);
+
+ template
+ void setValue(const QString &key, const T &value, SyncType sync = Sync);
+
+ QVariant defaultValue(const QString &key) const;
+
+ /*
+ * Remove the specified key from the section "settings"
+ */
+ void remove(const QString &key, SyncType sync = Sync);
+
+ void sync();
+
+ /*
+ * Available translations, hardcoded
+ */
+ QMap translations();
+
+private:
+ Settings();
+
+ /*
+ * Install some default values
+ */
+ void addDefaultValues();
+ void addDefaultValues(const QHash &defaultValues);
+
+ void fillTranslations();
+
+ QHash &defaultValues();
+
+ QSettings *settings() const;
+
+private:
+ SettingsPrivate *d;
+};
+
+/**********************************/
+
+template
+T Settings::value(const QString &key)
+{
+ T def = T();
+ QHash::iterator it = defaultValues().find(key);
+
+ if(it != defaultValues().end())
+ def = it.value().value();
+
+ return value(key, def);
+}
+
+template
+T Settings::value(const QString &key, const T &def)
+{
+ QSettings *s = settings();
+
+ s->beginGroup("settings");
+ QVariant value = s->value(key, QVariant::fromValue(def));
+ s->endGroup();
+
+ return value.value();
+}
+
+template
+void Settings::setValue(const QString &key, const T &value, Settings::SyncType sync)
+{
+ QSettings *s = settings();
+
+ s->beginGroup("settings");
+ s->setValue(key, QVariant::fromValue(value));
+ s->endGroup();
+
+ if(sync == Sync)
+ s->sync();
+}
+
+#endif // SETTINGS_H
diff --git a/squeezedlabel.cpp b/squeezedlabel.cpp
new file mode 100644
index 0000000..c742997
--- /dev/null
+++ b/squeezedlabel.cpp
@@ -0,0 +1,40 @@
+#include
+#include
+
+#include "squeezedlabel.h"
+
+SqueezedLabel::SqueezedLabel(QWidget *parent)
+ : QLabel(parent)
+ , m_metrics(nullptr)
+{
+ setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
+ setTextFormat(Qt::PlainText);
+
+ ensurePolished();
+}
+
+SqueezedLabel::~SqueezedLabel()
+{
+ delete m_metrics;
+}
+
+void SqueezedLabel::setText(const QString &text)
+{
+ m_text = text;
+ updateText();
+}
+
+void SqueezedLabel::updateText()
+{
+ if(!m_metrics)
+ m_metrics = new QFontMetrics(font());
+
+ QLabel::setText(m_metrics->elidedText(m_text, Qt::ElideMiddle, width()));
+}
+
+void SqueezedLabel::resizeEvent(QResizeEvent *event)
+{
+ Q_UNUSED(event)
+
+ updateText();
+}
diff --git a/squeezedlabel.h b/squeezedlabel.h
new file mode 100644
index 0000000..8633278
--- /dev/null
+++ b/squeezedlabel.h
@@ -0,0 +1,33 @@
+#ifndef SQUEEZEDLABEL_H
+#define SQUEEZEDLABEL_H
+
+#include
+
+class QFontMetrics;
+
+/*
+ * Label displaying dots if not enough space for all characters like
+ * long_image_...file.jpg
+ */
+class SqueezedLabel : public QLabel
+{
+ Q_OBJECT
+
+public:
+ explicit SqueezedLabel(QWidget *parent = 0);
+ ~SqueezedLabel();
+
+ void setText(const QString &text);
+
+private slots:
+ void updateText();
+
+protected:
+ virtual void resizeEvent(QResizeEvent *event) override;
+
+private:
+ QFontMetrics *m_metrics;
+ QString m_text;
+};
+
+#endif // SQUEEZEDLABEL_H
diff --git a/threadedimageloader.cpp b/threadedimageloader.cpp
new file mode 100644
index 0000000..8f9fdfa
--- /dev/null
+++ b/threadedimageloader.cpp
@@ -0,0 +1,20 @@
+#include
+#include
+
+#include "threadedimageloader.h"
+
+ThreadedImageLoader::ThreadedImageLoader(QObject *parent)
+ : QThread(parent)
+{}
+
+void ThreadedImageLoader::loadImageFromFile(const QString &imagePath)
+{
+ m_imagePath = imagePath;
+
+ start();
+}
+
+void ThreadedImageLoader::run()
+{
+ emit loaded(QImage(m_imagePath));
+}
diff --git a/threadedimageloader.h b/threadedimageloader.h
new file mode 100644
index 0000000..5b5fc8e
--- /dev/null
+++ b/threadedimageloader.h
@@ -0,0 +1,32 @@
+#ifndef THREADEDIMAGELOADER_H
+#define THREADEDIMAGELOADER_H
+
+#include
+#include
+
+class QImage;
+
+/*
+ * Thread to asynchronously load images. Allocates an image on a heap. Signal receiver
+ * MUST delete it
+ */
+class ThreadedImageLoader : public QThread
+{
+ Q_OBJECT
+
+public:
+ ThreadedImageLoader(QObject *parent = nullptr);
+
+ void loadImageFromFile(const QString &imagePath);
+
+protected:
+ virtual void run() override;
+
+signals:
+ void loaded(const QImage &);
+
+private:
+ QString m_imagePath;
+};
+
+#endif // THREADEDIMAGELOADER_H
diff --git a/utils.cpp b/utils.cpp
new file mode 100644
index 0000000..09ed4db
--- /dev/null
+++ b/utils.cpp
@@ -0,0 +1,108 @@
+#include
+#include
+
+#include
+
+#include "utils.h"
+
+QString Utils::helpString()
+{
+ return QObject::tr("Help");
+}
+
+QString Utils::informationString()
+{
+ return QObject::tr("Information");
+}
+
+QString Utils::warningString()
+{
+ return QObject::tr("Warning");
+}
+
+QString Utils::errorString()
+{
+ return QObject::tr("Error");
+}
+
+QString Utils::fatalErrorString()
+{
+ return QObject::tr("Fatal error");
+}
+
+QPoint Utils::centroid(const QPolygon &polygon)
+{
+ QPoint centroid{0,0};
+
+ foreach(const auto &point, polygon)
+ {
+ centroid.rx() += point.x();
+ centroid.ry() += point.y();
+ }
+
+ return centroid / polygon.size();
+}
+
+void Utils::setShortcutTooltips(const QList &buttons)
+{
+ // set tooltips
+ foreach(auto *button, buttons)
+ {
+ button->setToolTip(QObject::tr("Shortcut: %1").arg(button->shortcut().toString()));
+ }
+}
+
+QStringList Utils::imageMatchingWildcard()
+{
+ return QStringList()
+ << "*.jpg"
+ << "*.JPG"
+ << "*.jpeg"
+ << "*.JPEG"
+ << "*.png"
+ << "*.PNG"
+ ;
+}
+
+QPoint Utils::fitPoint(const QRect &m_referenceRect, const QPoint &_pos)
+{
+ QPoint pos = _pos;
+
+ if(pos.x() > m_referenceRect.topRight().x())
+ pos.setX(m_referenceRect.topRight().x());
+ else if(pos.x() < m_referenceRect.topLeft().x())
+ pos.setX(m_referenceRect.topLeft().x());
+
+ if(pos.y() > m_referenceRect.bottomLeft().y())
+ pos.setY(m_referenceRect.bottomLeft().y());
+ else if(pos.y() < m_referenceRect.topLeft().y())
+ pos.setY(m_referenceRect.topLeft().y());
+
+ return pos;
+}
+
+void Utils::help(const QString &message, QWidget *parent)
+{
+ QMessageBox::information(parent, Utils::helpString(), message);
+}
+
+void Utils::message(const QString &message, QWidget *parent)
+{
+ QMessageBox::information(parent, Utils::informationString(), message);
+}
+
+void Utils::warning(const QString &message, QWidget *parent)
+{
+ QMessageBox::warning(parent, Utils::warningString(), message);
+}
+
+void Utils::error(const QString &message, QWidget *parent)
+{
+ QMessageBox::critical(parent, Utils::errorString(), message);
+}
+
+void Utils::fatalError(const QString &message, QWidget *parent)
+{
+ QMessageBox::critical(parent, Utils::fatalErrorString(), message);
+ ::exit(1);
+}
diff --git a/utils.h b/utils.h
new file mode 100644
index 0000000..8916ebd
--- /dev/null
+++ b/utils.h
@@ -0,0 +1,59 @@
+#ifndef UTILS_H
+#define UTILS_H
+
+#include
+#include
+#include
+#include
+#include
+
+class QPushButton;
+class QPolygon;
+class QWidget;
+class QRect;
+
+class Utils
+{
+public:
+ static QString helpString();
+ static QString informationString();
+ static QString warningString();
+ static QString errorString();
+ static QString fatalErrorString();
+
+ /*
+ * Pen to pain a polygon
+ */
+ static QPen polygonPaintingPen();
+
+ /*
+ * Calculate a polygon centroid
+ */
+ static QPoint centroid(const QPolygon &polygon);
+
+ /*
+ * Set tooltips for buttons with their shortcuts
+ */
+ static void setShortcutTooltips(const QList &buttons);
+
+ /*
+ * Supported image formats as a UNIX wildcard
+ */
+ static QStringList imageMatchingWildcard();
+
+ static QPoint fitPoint(const QRect &m_referenceRect, const QPoint &_pos);
+
+ static void help(const QString &message, QWidget *parent = nullptr);
+ static void message(const QString &message, QWidget *parent = nullptr);
+ static void warning(const QString &message, QWidget *parent = nullptr);
+ static void error(const QString &message, QWidget *parent = nullptr);
+ static void fatalError(const QString &message, QWidget *parent = nullptr);
+};
+
+inline
+QPen Utils::polygonPaintingPen()
+{
+ return QPen(Qt::magenta, 2);
+}
+
+#endif // UTILS_H