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 +
QProgressIndicator.h
+ 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 +
imageviewerobserver.h
+ 1 +
+ + QProgressIndicator + QWidget +
QProgressIndicator.h
+ 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: + + + + + + + + + + + + + + 0 + 0 + 800 + 21 + + + + + File + + + + + + + + Edit + + + + + + Help + + + + + + + + + + + + + Open directory... + + + + + Preferences... + + + + + About... + + + + + Quit + + + + + Usage... + + + + + About Qt... + + + + + + + ImageView + QWidget +
imageview.h
+ 1 +
+ + SqueezedLabel + QLabel +
squeezedlabel.h
+
+
+ + + + 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