Initial commit.
39
CMakeLists.txt
Normal file
@ -0,0 +1,39 @@
|
||||
cmake_minimum_required(VERSION 2.8.11)
|
||||
|
||||
project(haselnuss)
|
||||
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
||||
# set qt specific options
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# set c++ 14
|
||||
set(CMAKE_CXX_STANDARD 14)
|
||||
|
||||
find_package(Qt5Widgets REQUIRED)
|
||||
find_package(Qt5Core REQUIRED)
|
||||
find_package(OpenCV REQUIRED)
|
||||
find_package(fann REQUIRED)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_search_module(JSONCPP REQUIRED jsoncpp)
|
||||
|
||||
# Source Files
|
||||
FILE(GLOB SOURCE_FILES "${CMAKE_SOURCE_DIR}/*.cpp")
|
||||
# Header files
|
||||
FILE(GLOB HEADER_FILES "${CMAKE_SOURCE_DIR}/*.h")
|
||||
|
||||
set(INCLUDE_DIRS ${INCLUDE_DIRS} "${CMAKE_SOURCE_DIR}")
|
||||
set(INCLUDE_DIRS ${INCLUDE_DIRS} "${FANN_INCLUDE_DIR}")
|
||||
|
||||
INCLUDE_DIRECTORIES("${INCLUDE_DIRS}")
|
||||
|
||||
# Libraries
|
||||
set(LIBRARIES ${LIBRARIES} ${QT_LIBRARIES})
|
||||
set(LIBRARIES ${LIBRARIES} ${OpenCV_LIBS})
|
||||
set(LIBRARIES ${LIBRARIES} ${FANN_LIBRARIES})
|
||||
set(LIBRARIES ${LIBRARIES} ${JSONCPP_LIBRARIES})
|
||||
|
||||
add_executable(haselnuss ${SOURCE_FILES} ${HEADER_FILES})
|
||||
qt5_use_modules(haselnuss Widgets)
|
||||
target_link_libraries(haselnuss ${LIBRARIES})
|
12
Makefile
Normal file
@ -0,0 +1,12 @@
|
||||
all: run
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
mkdir -p build
|
||||
cd build; cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ..; ninja
|
||||
|
||||
run: build
|
||||
./build/haselnuss
|
||||
|
||||
clean:
|
||||
rm -rf build
|
57
Readme.md
Normal file
@ -0,0 +1,57 @@
|
||||
#Rate my Haselnuss
|
||||
Visually guess the taste of a hazelnut
|
||||
|
||||
##Disclaimer
|
||||
|
||||
This program has been created as part of my image processing practical course
|
||||
at the FH Aachen. Therefore some parts of this are in german. If you're
|
||||
interested in this and have trouble understanding it, do not hesitate to
|
||||
contact me: I'll gladly translate it for you to english.
|
||||
|
||||
##Idee
|
||||
Dieses Programm versucht Haselnüsse visuell nach ihrem Geschmack zu
|
||||
klassifizieren, insbesondere die Erkennung der schlecht schmeckenden soll
|
||||
gelingen.
|
||||
|
||||
|
||||
##Technische Vorraussetzungen
|
||||
Dieses Programm benutzt CMake und kann wie jedes andere CMake-Projekt
|
||||
kompiliert werden:
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
```
|
||||
|
||||
Grundsätzlich sollte dieses Programm platformunabhängig sein. Getestet ist
|
||||
dieses jedoch nur unter Linux. Insbesondere die direkte Aufnahme der Fotos über
|
||||
die Android Debug Bridge (ADB) funktioniert nur unter Linux.
|
||||
|
||||
Folgende Libraries werden benötigt:
|
||||
* Qt5
|
||||
* OpenCV 3.1
|
||||
* fann 2.2 (Fast Artificial Neural Network Library,
|
||||
[http://leenissen.dk/fann/wp/]())
|
||||
* JsonCPP [https://github.com/open-source-parsers/jsoncpp]()
|
||||
|
||||
##Aufnahmebedingungen
|
||||
Aufnahmebedingungen für Eingabedaten:
|
||||
* Heller, einfarber Hintergrund mit einem H-Wert (im HSV-Farbraum) von über 70
|
||||
* Lockere Anordnung
|
||||
* Senkrechte Aufnahme aus ca. 18 cm Entfernung.
|
||||
* Senkrechte Beleuchtung (z.B. Blitz)
|
||||
* Auflösung des Bildsensors: 4160x3120
|
||||
|
||||
Alle Zahlenwerte können geändert werden, wenn die Konfiguration entsprechend
|
||||
angepasst wird. Die aktuelle Konfiguration ist allerdings nur unter diesen
|
||||
Bedingungen getestet. Abweichende Bedingungen können schlechtere Ergebnisse
|
||||
liefern.
|
||||
|
||||
##Bedienung
|
||||
Die Bedienung des Programmes sollte relativ selbsterklärend sein. Man läd ein
|
||||
Bild in die Applikation und erhält eine Klassifikation der Nüsse visuell
|
||||
angezeigt. Unter dem Konfigurationstab können die Erkennungs- und
|
||||
Bewertungsschritte nachvollzogen und angepasst werden.
|
||||
Einige Zahlenwerte können hier ebenfalls konfiguriert werden.
|
||||
|
375
config.json
Normal file
@ -0,0 +1,375 @@
|
||||
{
|
||||
"detection" :
|
||||
[
|
||||
{
|
||||
"input" : "color",
|
||||
"name" : "Verkleinern",
|
||||
"operation" : "_resize",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 512,
|
||||
"lowerBound" : 1,
|
||||
"name" : "Neue Seitenlänge",
|
||||
"odd" : false,
|
||||
"upperBound" : 10000
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "HSV Select",
|
||||
"operation" : "_thresh_color",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "untere Grenze",
|
||||
"odd" : false,
|
||||
"upperBound" : 360
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 70,
|
||||
"lowerBound" : 0,
|
||||
"name" : "obere Grenze",
|
||||
"odd" : false,
|
||||
"upperBound" : 360
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Dilatiere",
|
||||
"operation" : "_dilate",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 11,
|
||||
"lowerBound" : 1,
|
||||
"name" : "Kernel Größe",
|
||||
"odd" : true,
|
||||
"upperBound" : 100
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 1,
|
||||
"lowerBound" : 1,
|
||||
"name" : "Iterationen",
|
||||
"odd" : false,
|
||||
"upperBound" : 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Zurück zur Originalgröße",
|
||||
"operation" : "_resizeToOriginal",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Grenzwert",
|
||||
"operation" : "_threshold",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Markiere Hintergrund",
|
||||
"operation" : "_markBackground",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Invertieren",
|
||||
"operation" : "_invert_markers",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Erodieren",
|
||||
"operation" : "_erode_intermediate",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 11,
|
||||
"lowerBound" : 1,
|
||||
"name" : "Kernel Größe",
|
||||
"odd" : true,
|
||||
"upperBound" : 100
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 2,
|
||||
"lowerBound" : 1,
|
||||
"name" : "Iterationen",
|
||||
"odd" : false,
|
||||
"upperBound" : 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Zurück zur Originalgröße",
|
||||
"operation" : "_resizeToOriginal",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Grenzwert",
|
||||
"operation" : "_threshold",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Wasserscheide",
|
||||
"operation" : "_watershed",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"rating" : {
|
||||
"steps" :
|
||||
[
|
||||
{
|
||||
"input" : "color",
|
||||
"name" : "Grenzwert",
|
||||
"operation" : "_threshold",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Durschnittlicher H-Wert",
|
||||
"operation" : "_mean_color",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "color",
|
||||
"name" : "Grenzwert",
|
||||
"operation" : "_threshold",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Fit Ellipse",
|
||||
"operation" : "_fit_ellipse",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 220,
|
||||
"lowerBound" : 1,
|
||||
"name" : "Pixel pro Zentimeter",
|
||||
"odd" : false,
|
||||
"upperBound" : 10000
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "color",
|
||||
"name" : "Grenzwert nach Otsu",
|
||||
"operation" : "_thresh_otsu",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"input" : "previous",
|
||||
"name" : "Bewerte kleine Flecken",
|
||||
"operation" : "_rate_spots",
|
||||
"param1" :
|
||||
{
|
||||
"default" : 220,
|
||||
"lowerBound" : 1,
|
||||
"name" : "Pixel pro Zentimeter",
|
||||
"odd" : false,
|
||||
"upperBound" : 10000
|
||||
},
|
||||
"param2" :
|
||||
{
|
||||
"default" : 0,
|
||||
"lowerBound" : 0,
|
||||
"name" : "Ignoriert",
|
||||
"odd" : false,
|
||||
"upperBound" : 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"results" :
|
||||
[
|
||||
"avg_h_val",
|
||||
"size_diagonal",
|
||||
"size_ratio",
|
||||
"spot_avg_h_val",
|
||||
"spot_avg_size"
|
||||
]
|
||||
}
|
||||
}
|
452
controller.cpp
Normal file
@ -0,0 +1,452 @@
|
||||
#include "controller.h"
|
||||
#include <fstream>
|
||||
#include <QDebug>
|
||||
#include <QString>
|
||||
#include <set>
|
||||
#include <QFileDialog>
|
||||
#include <stdio.h>
|
||||
#include <chrono>
|
||||
#include "filenames.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace cv;
|
||||
|
||||
Controller::Controller(QObject* parent) : QObject(parent),
|
||||
detProc(), markers(detProc.getMarkers()) {
|
||||
string errors;
|
||||
ifstream file;
|
||||
file.open(FileNames::configName);
|
||||
bool ok = Json::parseFromStream(Json::CharReaderBuilder(),
|
||||
file, &config, &errors);
|
||||
file.close();
|
||||
if (!ok) {
|
||||
cerr << "Error while reading " << FileNames::configName
|
||||
<< ":\n" << errors << "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
detProc.init(config["detection"]);
|
||||
rateProc.init(config["rating"]["steps"]);
|
||||
net.init(config["rating"]["results"]);
|
||||
}
|
||||
|
||||
Controller& Controller::inst() {
|
||||
static Controller ctrlr;
|
||||
return ctrlr;
|
||||
}
|
||||
|
||||
const Mat* Controller::getCurImg() {
|
||||
return &cur_img;
|
||||
}
|
||||
|
||||
const Mat* Controller::getCurNut() {
|
||||
return &nuts[current_nut].img;
|
||||
}
|
||||
|
||||
void Controller::openImage() {
|
||||
filePath = QFileDialog::getOpenFileName(nullptr,
|
||||
tr("Öffnen"), "",
|
||||
tr("Bilder (*.png *.jpg)"));
|
||||
if (filePath.toStdString() == "") {
|
||||
return;
|
||||
}
|
||||
loadImage();
|
||||
}
|
||||
|
||||
void Controller::loadImage() {
|
||||
if ( !cur_img.empty() ){
|
||||
cur_img.copyTo(prev_img);
|
||||
}
|
||||
cur_img = imread(filePath.toStdString(), IMREAD_COLOR);
|
||||
emit statusMessage("Bild geladen ...");
|
||||
process();
|
||||
}
|
||||
|
||||
void Controller::process() {
|
||||
if (valid_nut) {
|
||||
if (nut_rating > 0) {
|
||||
emit statusMessage("Bewerte gegessene Nuss...");
|
||||
rateProc.reset();
|
||||
rateProc.setRateCurNut(false);
|
||||
rateProc.process();
|
||||
rateProc.setRateCurNut(true);
|
||||
debugResultPrint();
|
||||
net.saveNut();
|
||||
}
|
||||
else {
|
||||
cout << "No score selected. Not saving Nut." << endl;
|
||||
}
|
||||
}
|
||||
detProc.reset();
|
||||
emit newInputImage(cur_img);
|
||||
emit statusMessage("Erkenne Nüsse ...");
|
||||
if (!detect()) {
|
||||
return;
|
||||
}
|
||||
emit newInputImage(detProc.getResult());
|
||||
if (!prev_img.empty()) {
|
||||
vector<Nut> results = {};
|
||||
emit statusMessage("Extrahiere Nüsse ...");
|
||||
extractNuts(results);
|
||||
if (matchNuts(results)) {
|
||||
nuts = results;
|
||||
valid_nut = true;
|
||||
}
|
||||
else {
|
||||
valid_nut = false;
|
||||
nuts = results;
|
||||
}
|
||||
}
|
||||
else {
|
||||
emit statusMessage("Extrahiere Nüsse ...");
|
||||
extractNuts();
|
||||
}
|
||||
nut_rating = -1;
|
||||
//debugPaint();
|
||||
emit statusMessage("Bewerte Nüsse ...");
|
||||
Mat withScores = Mat::zeros(cur_img.size(), CV_8UC3);
|
||||
for (int i = 0; i<nuts.size(); i++) {
|
||||
current_nut = i;
|
||||
rateProc.reset();
|
||||
rateProc.process();
|
||||
float score = net.run(rateProc.getResults());
|
||||
nuts[current_nut].score = score;
|
||||
const vector<vector<Point>>& contours = rateProc.getContours();
|
||||
vector<Rect> rects(contours.size());
|
||||
for (unsigned int i = 0; i<contours.size(); i++) {
|
||||
Rect size = boundingRect(contours[i]);
|
||||
rects[i] = size;
|
||||
}
|
||||
int max_rect_index = -1;
|
||||
int max_rect_size = 0;
|
||||
for (unsigned int i = 0; i<rects.size(); i++) {
|
||||
if (rects[i].height * rects[i].width > max_rect_size) {
|
||||
max_rect_size = rects[i].height * rects[i].width;
|
||||
max_rect_index = i;
|
||||
}
|
||||
}
|
||||
drawContours(withScores, contours, max_rect_index,
|
||||
Scalar(static_cast<uchar>(score * 100.0f *0.708333f), 255, 255),
|
||||
CV_FILLED, LINE_8, noArray(), INT_MAX,
|
||||
Point(nuts[current_nut].min_y, nuts[current_nut].min_x));
|
||||
}
|
||||
emit statusMessage("Zeige Ergebnisse an ...");
|
||||
cvtColor(withScores, withScores, CV_HSV2BGR);
|
||||
withScores = 0.5 *withScores + 0.5 * cur_img;
|
||||
paintScores(withScores);
|
||||
emit statusMessage("Fertig.");
|
||||
}
|
||||
|
||||
void Controller::debugResultPrint() {
|
||||
ostringstream sstr;
|
||||
sstr << "Results: ";
|
||||
for (const auto& result : rateProc.getResults()) {
|
||||
sstr << result.first << ": " << result.second << ", ";
|
||||
}
|
||||
cout << sstr.str() << endl;
|
||||
}
|
||||
|
||||
void Controller::paintScores(Mat& img) {
|
||||
for (unsigned int i = 0; i<nuts.size(); i++){
|
||||
ostringstream sstr;
|
||||
sstr.precision(1);
|
||||
sstr << nuts[i].score;
|
||||
string text = sstr.str();
|
||||
int font = FONT_HERSHEY_SIMPLEX;
|
||||
double fontScale = 3.0;
|
||||
int thickness = 5;
|
||||
int baseline = 0;
|
||||
Size textSize = getTextSize(text, font, fontScale, thickness,
|
||||
&baseline);
|
||||
baseline += thickness;
|
||||
Point textOrg(nuts[i].center_y() - 0.5 * textSize.width,
|
||||
nuts[i].center_x() - 0.5 * textSize.height);
|
||||
putText(img, text, textOrg,
|
||||
font, fontScale, Scalar::all(255), thickness);
|
||||
circle(img, Point(nuts[i].center_y(), nuts[i].center_x()), 5,
|
||||
Scalar::all(255), 5);
|
||||
}
|
||||
emit newInputImage(img);
|
||||
}
|
||||
|
||||
void Controller::debugPaint() {
|
||||
Mat withText = detProc.getResult().clone();
|
||||
for (unsigned int i = 0; i<nuts.size(); i++){
|
||||
string text = to_string(i);
|
||||
int font = FONT_HERSHEY_SIMPLEX;
|
||||
double fontScale = 3.0;
|
||||
int thickness = 5;
|
||||
int baseline = 0;
|
||||
Size textSize = getTextSize(text, font, fontScale, thickness,
|
||||
&baseline);
|
||||
baseline += thickness;
|
||||
Point textOrg(nuts[i].center_y() - 0.5 * textSize.width,
|
||||
nuts[i].center_x() - 0.5 * textSize.height);
|
||||
putText(withText, text, textOrg,
|
||||
font, fontScale, Scalar::all(255), thickness);
|
||||
circle(withText, Point(nuts[i].center_y(), nuts[i].center_x()), 5,
|
||||
Scalar::all(255), 5);
|
||||
}
|
||||
emit newInputImage(withText);
|
||||
}
|
||||
|
||||
void Controller::captureImage() {
|
||||
system(
|
||||
"adb shell \"count=\\$(ls /mnt/sdcard/DCIM/CAMERA | wc -l) && "
|
||||
"am start -a android.media.action.STILL_IMAGE_CAMERA && "
|
||||
"sleep 1 && "
|
||||
"input keyevent 27 && "
|
||||
"while [ \\$count = \\$(ls /mnt/sdcard/DCIM/CAMERA | wc -l) ]; do "
|
||||
"sleep 0.5; "
|
||||
"echo 'not done'; done; "
|
||||
"echo done\"");
|
||||
string img = runCmd("adb shell \"ls /mnt/sdcard/DCIM/CAMERA | "
|
||||
"grep -v VID | tail -n 1\"");
|
||||
// remove white space
|
||||
img.erase(std::remove_if(img.begin(), img.end(), ::isspace), img.end());
|
||||
if (!transferFolder.isValid()) {
|
||||
cerr << "Failed to create temporary folder. Exiting.";
|
||||
exit(1);
|
||||
}
|
||||
string cmd = "adb pull \"/mnt/sdcard/DCIM/CAMERA/" + img + "\" \""
|
||||
+ transferFolder.path().toStdString() + "/\"";
|
||||
system(cmd.c_str());
|
||||
system(("adb shell \"rm /mnt/sdcard/DCIM/CAMERA/" + img + "\"").c_str());
|
||||
filePath = transferFolder.path() + "/" + QString::fromStdString(img);
|
||||
loadImage();
|
||||
remove(filePath.toStdString().c_str());
|
||||
}
|
||||
|
||||
bool Controller::detect() {
|
||||
auto start = chrono::steady_clock::now();
|
||||
bool success = detProc.process();
|
||||
if (!success) {
|
||||
cerr << "Error during detection." << endl;
|
||||
return success;
|
||||
}
|
||||
auto duration = chrono::duration_cast<chrono::milliseconds>(
|
||||
chrono::steady_clock::now() - start);
|
||||
cout << "Detection took " << (float)duration.count()/1000.0
|
||||
<< " seconds." << endl;
|
||||
return success;
|
||||
}
|
||||
|
||||
bool Controller::matchNuts(const vector<Nut>& cur_results) {
|
||||
auto start = chrono::steady_clock::now();
|
||||
vector<float> min_dists = vector<float>(nuts.size());
|
||||
int count = 0;
|
||||
for(const auto& nut : nuts) {
|
||||
vector<float> x_dist = vector<float>(cur_results.size());
|
||||
for (unsigned int i = 0; i<cur_results.size(); i++) {
|
||||
x_dist[i] = abs(cur_results[i].center_x() - nut.center_x());
|
||||
}
|
||||
vector<float> y_dist = vector<float>(cur_results.size());
|
||||
for (unsigned int i = 0; i<cur_results.size(); i++) {
|
||||
y_dist[i] = abs(cur_results[i].center_y() - nut.center_y());
|
||||
}
|
||||
vector<float> dist = vector<float>(cur_results.size());
|
||||
magnitude(x_dist, y_dist, dist);
|
||||
auto min = min_element(dist.begin(), dist.end());
|
||||
min_dists[count] = *min;
|
||||
cout << "Minimum distance for nut " << count <<
|
||||
": " << *min
|
||||
<< " ("
|
||||
<< (*min/
|
||||
(sqrt(pow(static_cast<float>(cur_img.cols), 2) +
|
||||
pow(static_cast<float>(cur_img.rows), 2))))*100.0
|
||||
<< "%)\n";
|
||||
count++;
|
||||
}
|
||||
bool success = false;
|
||||
if (cur_results.size() == nuts.size() - 1) {
|
||||
success = true;
|
||||
int nut_index;
|
||||
auto max = max_element(min_dists.begin(), min_dists.end());
|
||||
nut_index = distance(min_dists.begin(), max);
|
||||
cout << "Nut " << nut_index << " has been taken away.\n";
|
||||
nuts[nut_index].img.copyTo(nut_to_rate);
|
||||
emit newNutToRate(nut_to_rate);
|
||||
}
|
||||
else {
|
||||
emit message("Keine Nuss erkannt");
|
||||
}
|
||||
auto duration = chrono::duration_cast<chrono::milliseconds>(
|
||||
chrono::steady_clock::now() - start);
|
||||
cout << "Matching took " << (float)duration.count()/1000.0
|
||||
<< " seconds." << endl;
|
||||
return success;
|
||||
}
|
||||
|
||||
void Controller::extractNuts(vector<Nut>& result_nuts) {
|
||||
auto start = chrono::steady_clock::now();
|
||||
if (result_nuts.size() > 0) {
|
||||
return;
|
||||
}
|
||||
if (markers.empty()) {
|
||||
if (!detProc.process()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
int nut_count = detProc.getNutCount();
|
||||
result_nuts = vector<Nut>(nut_count);
|
||||
for (unsigned int i = 0; i<result_nuts.size(); i++) {
|
||||
result_nuts[i].min_x = markers.rows;
|
||||
result_nuts[i].min_y = markers.cols;
|
||||
result_nuts[i].max_x = 0;
|
||||
result_nuts[i].max_y = 0;
|
||||
}
|
||||
for(int i = 0; i<markers.rows; i++) {
|
||||
for(int j = 0; j<markers.cols; j++) {
|
||||
int index = markers.at<int>(i, j) -2;
|
||||
if (index >= 0 && index < nut_count) {
|
||||
result_nuts[index].min_x = min(result_nuts[index].min_x, i);
|
||||
result_nuts[index].min_y = min(result_nuts[index].min_y, j);
|
||||
result_nuts[index].max_x = max(result_nuts[index].max_x, i);
|
||||
result_nuts[index].max_y = max(result_nuts[index].max_y, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (unsigned int i = 0; i<result_nuts.size(); i++) {
|
||||
result_nuts[i].img = Mat::zeros(result_nuts[i].max_x - result_nuts[i].min_x,
|
||||
result_nuts[i].max_y - result_nuts[i].min_y, CV_8UC3);
|
||||
}
|
||||
for(int i = 0; i<markers.rows; i++) {
|
||||
for(int j = 0; j<markers.cols; j++) {
|
||||
int index = markers.at<int>(i, j)-2;
|
||||
if (index >= 0 && index < nut_count) {
|
||||
result_nuts[index].img.at<Vec3b>(i - result_nuts[index].min_x,
|
||||
j - result_nuts[index].min_y) = cur_img.at<Vec3b>(i, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto duration = chrono::duration_cast<chrono::milliseconds>(
|
||||
chrono::steady_clock::now() - start);
|
||||
cout << "Nut extraction took " << (float)duration.count()/1000.0
|
||||
<< " seconds.\n";
|
||||
cout << "Detected " << result_nuts.size() << " nuts." << endl;
|
||||
rateProc.reset();
|
||||
if (rateProc.isInteractive()) {
|
||||
emit newNut(result_nuts[current_nut].img);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::nextNut() {
|
||||
if (nuts.size() == 0 || nuts[current_nut].img.empty()) {
|
||||
return;
|
||||
}
|
||||
int size = nuts.size();
|
||||
current_nut = min(current_nut + 1, size-1);
|
||||
newNut();
|
||||
if (rateProc.isInteractive()) {
|
||||
emit newNut(nuts[current_nut].img);
|
||||
emit rateProc.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::previousNut() {
|
||||
if (nuts.size() == 0 || nuts[current_nut].img.empty()) {
|
||||
return;
|
||||
}
|
||||
current_nut = max(current_nut - 1, 0);
|
||||
newNut();
|
||||
if (rateProc.isInteractive()) {
|
||||
emit newNut(nuts[current_nut].img);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::newNut() {
|
||||
rateProc.reset();
|
||||
if (rateProc.isInteractive()) {
|
||||
rateProc.start(true);
|
||||
}
|
||||
}
|
||||
|
||||
string Controller::runCmd(string cmd) {
|
||||
char buffer[128];
|
||||
string result = "";
|
||||
shared_ptr<FILE> pipe(popen(cmd.c_str(), "r"), pclose);
|
||||
if (!pipe) throw runtime_error("popen() failed!");
|
||||
while (!feof(pipe.get())) {
|
||||
if (fgets(buffer, 128, pipe.get()) != NULL)
|
||||
result += buffer;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Controller::saveDetectionConfig() {
|
||||
int current_step = detProc.getCurrentStep();
|
||||
config["detection"][current_step]["param1"]["default"] =
|
||||
detProc.getCurParams()[0].val;
|
||||
config["detection"][current_step]["param2"]["default"] =
|
||||
detProc.getCurParams()[1].val;
|
||||
ofstream file ("config.json");
|
||||
file << Json::writeString(Json::StreamWriterBuilder(), config);
|
||||
file.close();
|
||||
}
|
||||
|
||||
void Controller::saveRatingConfig() {
|
||||
int current_step = rateProc.getCurrentStep();
|
||||
config["rating"]["steps"][current_step]["param1"]["default"] =
|
||||
rateProc.getCurParams()[0].val;
|
||||
config["rating"]["steps"][current_step]["param2"]["default"] =
|
||||
rateProc.getCurParams()[1].val;
|
||||
ofstream file ("config.json");
|
||||
file << Json::writeString(Json::StreamWriterBuilder(), config);
|
||||
file.close();
|
||||
}
|
||||
|
||||
void Controller::reset() {
|
||||
resetNuts();
|
||||
detProc.reset();
|
||||
rateProc.reset();
|
||||
cur_img.release();
|
||||
prev_img.release();
|
||||
Mat empty;
|
||||
emit newInputImage(empty);
|
||||
emit newNutToRate(empty);
|
||||
}
|
||||
|
||||
void Controller::resetNuts() {
|
||||
current_nut = 0;
|
||||
nut_rating = -1;
|
||||
nuts = {};
|
||||
}
|
||||
|
||||
void Controller::saveData() {
|
||||
cout << "... saving data ... " << endl;
|
||||
if (valid_nut) {
|
||||
if (nut_rating > 0) {
|
||||
net.saveNut();
|
||||
}
|
||||
else {
|
||||
cout << "No score selected. Not saving Nut." << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::currentRating(int score) {
|
||||
nut_rating = score;
|
||||
}
|
||||
|
||||
int Controller::getCurRating() {
|
||||
return nut_rating;
|
||||
}
|
||||
|
||||
int Controller::getN() {
|
||||
return net.getN();
|
||||
}
|
||||
|
||||
void Controller::neuralNetChange(int val) {
|
||||
if (val == Qt::Unchecked) {
|
||||
net.neuralNetEnabled(false);
|
||||
process();
|
||||
}
|
||||
else if (val == Qt::Checked) {
|
||||
net.neuralNetEnabled(true);
|
||||
process();
|
||||
}
|
||||
}
|
||||
|
||||
const cv::Mat* Controller::getNutToRate() {
|
||||
return &nut_to_rate;
|
||||
}
|
103
controller.h
Normal file
@ -0,0 +1,103 @@
|
||||
#ifndef CONTROLLER_H
|
||||
#define CONTROLLER_H
|
||||
|
||||
#include "detectionprocessing.h"
|
||||
#include "rateprocessing.h"
|
||||
#include "neuralnet.h"
|
||||
#include <json/json.h>
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
#include <QTemporaryDir>
|
||||
#include <QCloseEvent>
|
||||
|
||||
struct Nut {
|
||||
int min_x;
|
||||
int min_y;
|
||||
int max_x;
|
||||
int max_y;
|
||||
cv::Mat img;
|
||||
float score;
|
||||
|
||||
int center_x() const {
|
||||
return min_x + ((max_x - min_x)/2);
|
||||
}
|
||||
|
||||
int center_y() const {
|
||||
return min_y + ((max_y - min_y)/2);
|
||||
}
|
||||
};
|
||||
|
||||
class Controller : public QObject {
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DetectionProcessing& getDetectionProcessing() { return detProc; }
|
||||
RateProcessing& getRateProcessing() { return rateProc; }
|
||||
void extractNuts(std::vector<Nut>& result_nuts = inst().nuts);
|
||||
const cv::Mat* getCurImg();
|
||||
const cv::Mat* getCurNut();
|
||||
const cv::Mat* getNutToRate();
|
||||
void process();
|
||||
void resetNuts();
|
||||
int getCurRating();
|
||||
int getN();
|
||||
//Singleton
|
||||
static Controller& inst();
|
||||
Controller(Controller const&) = delete;
|
||||
void operator=(Controller const&) = delete;
|
||||
|
||||
signals:
|
||||
void newNut(const cv::Mat& nut);
|
||||
void newInputImage(const cv::Mat& img);
|
||||
void newNutToRate(const cv::Mat& nut);
|
||||
void message(std::string text);
|
||||
void statusMessage(QString);
|
||||
|
||||
public slots:
|
||||
void nextNut();
|
||||
void previousNut();
|
||||
void openImage();
|
||||
void captureImage();
|
||||
void saveDetectionConfig();
|
||||
void saveRatingConfig();
|
||||
void reset();
|
||||
void saveData();
|
||||
void currentRating(int score);
|
||||
void neuralNetChange(int val);
|
||||
|
||||
// private methods
|
||||
private:
|
||||
std::string runCmd(std::string cmd);
|
||||
void loadImage();
|
||||
bool detect();
|
||||
void newNut();
|
||||
bool matchNuts(const std::vector<Nut>& cur_results);
|
||||
void debugPaint();
|
||||
void debugResultPrint();
|
||||
void paintScores(cv::Mat& img);
|
||||
|
||||
// members
|
||||
private:
|
||||
// Singleton
|
||||
Controller(QObject* parent = nullptr);
|
||||
|
||||
DetectionProcessing detProc;
|
||||
RateProcessing rateProc;
|
||||
NeuralNet net;
|
||||
Json::Value config;
|
||||
std::vector<Nut> nuts{};
|
||||
int current_nut = 0;
|
||||
const cv::Mat& markers;
|
||||
cv::Mat prev_img;
|
||||
cv::Mat cur_img;
|
||||
static Controller *singleton;
|
||||
QTemporaryDir transferFolder;
|
||||
QString filePath;
|
||||
cv::Mat nut_to_rate;
|
||||
int nut_rating = -1;
|
||||
bool valid_nut = false;
|
||||
};
|
||||
|
||||
#endif // CONTROLLER_H
|
123
cvimagewidget.h
Normal file
@ -0,0 +1,123 @@
|
||||
#ifndef CVIMAGEWIDGET_H
|
||||
#define CVIMAGEWIDGET_H
|
||||
#include <QWidget>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QResizeEvent>
|
||||
#include <QDebug>
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
class CVImageWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CVImageWidget(QWidget *parent = 0) : QWidget(parent) {}
|
||||
|
||||
QSize sizeHint() const { return _qimage.size(); }
|
||||
QSize minimumSizeHint() const { return QSize(400, 400); }
|
||||
|
||||
public slots:
|
||||
|
||||
void showImage(const cv::Mat& image) {
|
||||
if (image.empty()) {
|
||||
_qimage = QImage();
|
||||
repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
cv::Mat tmp;
|
||||
if (image.type() == CV_32S) {
|
||||
image.convertTo(tmp, CV_8U);
|
||||
}
|
||||
else {
|
||||
tmp = image.clone();
|
||||
}
|
||||
|
||||
// Scale image to 512 pixels
|
||||
float fac = 1.0;
|
||||
if (size().width() > size().height()) {
|
||||
fac = (float) size().height()/image.rows;
|
||||
}
|
||||
else {
|
||||
fac = (float) size().width()/image.cols;
|
||||
}
|
||||
cv::Size new_size((int)image.cols*fac, (int) image.rows*fac) ;
|
||||
cv::resize(tmp, _tmp, new_size);
|
||||
// Convert the image to the RGB888 format
|
||||
switch (_tmp.type()) {
|
||||
case CV_8UC1:
|
||||
cvtColor(_tmp, _tmp, CV_GRAY2RGB);
|
||||
break;
|
||||
case CV_8UC3:
|
||||
cvtColor(_tmp, _tmp, CV_BGR2RGB);
|
||||
break;
|
||||
}
|
||||
|
||||
// QImage needs the data to be stored continuously in memory
|
||||
assert(_tmp.isContinuous());
|
||||
// Assign OpenCV's image buffer to the QImage. Note that the
|
||||
// bytesPerLine parameter
|
||||
// (http://qt-project.org/doc/qt-4.8/qimage.html#QImage-6) is 3*width
|
||||
// because each pixel has three bytes.
|
||||
_qimage = QImage(_tmp.data, _tmp.cols, _tmp.rows, _tmp.cols*3,
|
||||
QImage::Format_RGB888);
|
||||
repaint();
|
||||
}
|
||||
|
||||
void resizeEvent(QResizeEvent* event) {
|
||||
if (_tmp.empty()) {
|
||||
return;
|
||||
}
|
||||
float fac = 1.0;
|
||||
if (event->size().width() > event->size().height()) {
|
||||
fac = static_cast<float>(event->size().height())/
|
||||
static_cast<float>(_tmp.rows);
|
||||
}
|
||||
else {
|
||||
fac = static_cast<float>(event->size().width())/
|
||||
static_cast<float>(_tmp.cols);
|
||||
}
|
||||
cv::Size new_size((int)_tmp.cols*fac, (int) _tmp.rows*fac) ;
|
||||
cv::resize(_tmp, _tmp, new_size);
|
||||
// Assign OpenCV's image buffer to the QImage. Note that the
|
||||
// bytesPerLine parameter
|
||||
// (http://qt-project.org/doc/qt-4.8/qimage.html#QImage-6) is 3*width
|
||||
// because each pixel has three bytes.
|
||||
_qimage = QImage(_tmp.data, _tmp.cols, _tmp.rows, _tmp.cols*3,
|
||||
QImage::Format_RGB888);
|
||||
repaint();
|
||||
}
|
||||
|
||||
void showTextImage(std::string text) {
|
||||
int fontFace = cv::FONT_HERSHEY_SIMPLEX;
|
||||
double fontScale = 2;
|
||||
int thickness = 3;
|
||||
cv::Mat img(600, 800, CV_8UC3, cv::Scalar::all(0));
|
||||
int baseline=0;
|
||||
cv::Size textSize = cv::getTextSize(text, fontFace,
|
||||
fontScale, thickness, &baseline);
|
||||
baseline += thickness;
|
||||
// center the text
|
||||
cv::Point textOrg((img.cols - textSize.width)/2,
|
||||
(img.rows + textSize.height)/2);
|
||||
// then put the text itself
|
||||
cv::putText(img, text, textOrg, fontFace, fontScale,
|
||||
cv::Scalar::all(255), thickness, 8);
|
||||
showImage(img);
|
||||
}
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
void paintEvent(QPaintEvent* /*event*/) {
|
||||
// Display the image
|
||||
QPainter painter(this);
|
||||
painter.drawImage(QPoint(0,0), _qimage);
|
||||
painter.end();
|
||||
}
|
||||
|
||||
QImage _qimage;
|
||||
cv::Mat _tmp;
|
||||
};
|
||||
|
||||
#endif //CVIMAGEWIDGET_H
|
1
data/imgs/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
!*.jpg
|
BIN
data/imgs/nut_0.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
data/imgs/nut_1.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
data/imgs/nut_10.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
data/imgs/nut_11.jpg
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
data/imgs/nut_12.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
data/imgs/nut_13.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
data/imgs/nut_14.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
data/imgs/nut_15.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
data/imgs/nut_16.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
data/imgs/nut_17.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
data/imgs/nut_18.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
data/imgs/nut_19.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
data/imgs/nut_2.jpg
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
data/imgs/nut_20.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
data/imgs/nut_21.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
data/imgs/nut_22.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
data/imgs/nut_23.jpg
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
data/imgs/nut_24.jpg
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
data/imgs/nut_25.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
data/imgs/nut_26.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
data/imgs/nut_3.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
data/imgs/nut_4.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
data/imgs/nut_5.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
data/imgs/nut_6.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
data/imgs/nut_7.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
data/imgs/nut_8.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
data/imgs/nut_9.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
28
data/input.csv
Normal file
@ -0,0 +1,28 @@
|
||||
nut_id;avg_h_val;size_diagonal;size_ratio;spot_avg_h_val;spot_avg_size;
|
||||
0;2.15543448442352208e+01;2.40840983390808105e+00;8.30784440040588379e-01;4.00582693340368650e+01;2.89089112145802969e-02;
|
||||
1;2.85995685587164630e+01;2.12009000778198242e+00;8.76522243022918701e-01;7.05022463391572387e+01;3.81656800707181332e-02;
|
||||
2;2.08126407875761288e+01;2.22104597091674805e+00;9.02630627155303955e-01;3.27760247141309264e+01;4.23160610679326546e-02;
|
||||
3;2.27827500793902828e+01;2.21160554885864258e+00;8.62763643264770508e-01;6.35754374050604554e+01;4.00986809268770519e-02;
|
||||
4;2.20254226337891694e+01;2.41725397109985352e+00;8.32525730133056641e-01;8.86535834821747954e+01;3.96018549923069160e-02;
|
||||
5;2.07578218819002380e+01;2.16546726226806641e+00;8.67750287055969238e-01;7.40295570873757924e+01;3.84405589464939038e-02;
|
||||
6;2.25812620460063584e+01;2.08592319488525391e+00;9.63612735271453857e-01;5.31947450825757286e+01;4.16234488977769920e-02;
|
||||
7;2.86359098228663456e+01;2.11695909500122070e+00;9.49448287487030029e-01;5.78029675840922295e+01;4.10201069878011818e-02;
|
||||
8;2.23608877125114525e+01;2.49507260322570801e+00;8.32136988639831543e-01;3.31328554199994656e+01;5.39600710316402166e-02;
|
||||
9;2.53518362580978369e+01;2.31170797348022461e+00;8.76543164253234863e-01;6.94130929198291398e+01;4.70851360889505757e-02;
|
||||
10;2.38376188964527991e+01;2.22326540946960449e+00;9.20266449451446533e-01;6.81529845130824015e+01;4.10076653183280640e-02;
|
||||
11;2.67171104065323135e+01;2.49273753166198730e+00;7.78337955474853516e-01;8.10785295169221598e+01;9.83759143393793112e-02;
|
||||
12;2.60709829962772908e+01;2.07745194435119629e+00;9.66803669929504395e-01;6.17339857042509621e+01;4.32154684796301619e-02;
|
||||
13;1.98233052030320600e+01;2.34185457229614258e+00;8.92356693744659424e-01;4.56226145591685039e+01;6.26236441603117561e-02;
|
||||
14;2.38743842936802970e+01;2.22323918342590332e+00;9.26075160503387451e-01;4.12464626640129737e+01;3.89743338028589875e-02;
|
||||
15;2.16857333502065046e+01;2.25483775138854980e+00;9.00114536285400391e-01;1.01177834562342255e+02;3.99716212108672322e-02;
|
||||
16;2.74209935773427240e+01;2.14553141593933105e+00;9.03680801391601562e-01;5.13987904907524182e+01;6.88065268154826465e-02;
|
||||
17;2.73438467310758853e+01;2.40109395980834961e+00;7.81638681888580322e-01;4.54371387821523456e+01;4.56836996656475661e-02;
|
||||
18;2.65054871709301878e+01;2.27910423278808594e+00;7.89494752883911133e-01;7.47637672686023649e+01;6.18897987609845074e-02;
|
||||
19;2.03020797482668769e+01;2.33886933326721191e+00;7.77873873710632324e-01;5.60728065154369801e+01;4.25227099943926018e-02;
|
||||
20;2.23449875088078933e+01;2.20789957046508789e+00;8.45886528491973877e-01;7.81261270189904735e+01;5.70639237309947187e-02;
|
||||
21;2.24425214007782117e+01;2.36169195175170898e+00;9.09197628498077393e-01;3.87564170377607482e+01;6.99308254502036403e-02;
|
||||
22;2.55443989631836352e+01;2.14459204673767090e+00;9.73132252693176270e-01;5.02639359259216931e+01;5.56839998271064091e-02;
|
||||
23;2.14019211549139357e+01;2.33309888839721680e+00;8.76630365848541260e-01;3.50631278883680224e+01;5.73160344904119351e-02;
|
||||
24;2.55478804441722041e+01;2.03121805191040039e+00;8.49647045135498047e-01;3.12149577973704666e+01;3.17756131700512243e-02;
|
||||
25;2.58053171628290663e+01;1.97867727279663086e+00;9.44334387779235840e-01;5.90922410389039996e+01;3.49965437046893188e-02;
|
||||
26;1.79281499888120770e+01;2.26088070869445801e+00;8.75540912151336670e-01;3.12600847616190833e+01;3.33142616346284934e-02;
|
|
36
data/nuts.net
Normal file
@ -0,0 +1,36 @@
|
||||
FANN_FLO_2.1
|
||||
num_layers=3
|
||||
learning_rate=0.700000
|
||||
connection_rate=1.000000
|
||||
network_type=0
|
||||
learning_momentum=0.000000
|
||||
training_algorithm=1
|
||||
train_error_function=1
|
||||
train_stop_function=0
|
||||
cascade_output_change_fraction=0.010000
|
||||
quickprop_decay=-0.000100
|
||||
quickprop_mu=1.750000
|
||||
rprop_increase_factor=1.200000
|
||||
rprop_decrease_factor=0.500000
|
||||
rprop_delta_min=0.000000
|
||||
rprop_delta_max=50.000000
|
||||
rprop_delta_zero=0.100000
|
||||
cascade_output_stagnation_epochs=12
|
||||
cascade_candidate_change_fraction=0.010000
|
||||
cascade_candidate_stagnation_epochs=12
|
||||
cascade_max_out_epochs=150
|
||||
cascade_min_out_epochs=50
|
||||
cascade_max_cand_epochs=150
|
||||
cascade_min_cand_epochs=50
|
||||
cascade_num_candidate_groups=2
|
||||
bit_fail_limit=3.49999994039535522461e-01
|
||||
cascade_candidate_limit=1.00000000000000000000e+03
|
||||
cascade_weight_multiplier=4.00000005960464477539e-01
|
||||
cascade_activation_functions_count=10
|
||||
cascade_activation_functions=3 5 7 8 10 11 14 15 16 17
|
||||
cascade_activation_steepnesses_count=4
|
||||
cascade_activation_steepnesses=2.50000000000000000000e-01 5.00000000000000000000e-01 7.50000000000000000000e-01 1.00000000000000000000e+00
|
||||
layer_sizes=6 7 2
|
||||
scale_included=0
|
||||
neurons (num_inputs, activation_function, activation_steepness)=(0, 0, 0.00000000000000000000e+00) (0, 0, 0.00000000000000000000e+00) (0, 0, 0.00000000000000000000e+00) (0, 0, 0.00000000000000000000e+00) (0, 0, 0.00000000000000000000e+00) (0, 0, 0.00000000000000000000e+00) (6, 3, 5.00000000000000000000e-01) (6, 3, 5.00000000000000000000e-01) (6, 3, 5.00000000000000000000e-01) (6, 3, 5.00000000000000000000e-01) (6, 3, 5.00000000000000000000e-01) (6, 3, 5.00000000000000000000e-01) (0, 3, 0.00000000000000000000e+00) (7, 0, 5.00000000000000000000e-01) (0, 0, 0.00000000000000000000e+00)
|
||||
connections (connected_to_neuron, weight)=(0, -2.89685893058776855469e+00) (1, -2.21340131759643554688e+00) (2, -6.53899073600769042969e-01) (3, 5.05288600921630859375e-01) (4, -2.12488818168640136719e+00) (5, -3.21228432655334472656e+00) (0, -2.64115977287292480469e+00) (1, 4.38077020645141601562e+00) (2, -2.23532605171203613281e+00) (3, 2.82412099838256835938e+00) (4, -1.78801298141479492188e+00) (5, -2.15510916709899902344e+00) (0, -1.28378415107727050781e+00) (1, -4.66057968139648437500e+00) (2, -3.13344097137451171875e+00) (3, -3.34458440542221069336e-01) (4, 5.57140636444091796875e+00) (5, 4.77392911911010742188e+00) (0, -2.00398230552673339844e+00) (1, 1.77709951996803283691e-01) (2, -4.12612724304199218750e+00) (3, -4.50434893369674682617e-01) (4, 4.67733287811279296875e+00) (5, -7.28608146309852600098e-02) (0, -1.84619712829589843750e+00) (1, -1.72842276096343994141e+00) (2, -3.57149934768676757812e+00) (3, -1.81574237346649169922e+00) (4, 6.00842870771884918213e-02) (5, -2.67083120346069335938e+00) (0, -4.89023780822753906250e+00) (1, -6.48415279388427734375e+00) (2, -3.84839057922363281250e+00) (3, 3.94571590423583984375e+00) (4, -3.12683850526809692383e-01) (5, 3.10192298889160156250e+00) (6, -5.09613215923309326172e-01) (7, 3.94862794876098632812e+00) (8, 6.27435493469238281250e+00) (9, -4.65998220443725585938e+00) (10, -4.07627582550048828125e-01) (11, -2.93442416191101074219e+00) (12, -3.50238943099975585938e+00)
|
28
data/output.csv
Normal file
@ -0,0 +1,28 @@
|
||||
nut_id;score;
|
||||
0;2;
|
||||
1;4;
|
||||
2;4;
|
||||
3;4;
|
||||
4;5;
|
||||
5;3;
|
||||
6;3;
|
||||
7;1;
|
||||
8;2;
|
||||
9;3;
|
||||
10;1;
|
||||
11;2;
|
||||
12;3;
|
||||
13;4;
|
||||
14;1;
|
||||
15;4;
|
||||
16;5;
|
||||
17;3;
|
||||
18;3;
|
||||
19;4;
|
||||
20;3;
|
||||
21;3;
|
||||
22;4;
|
||||
23;2;
|
||||
24;4;
|
||||
25;4;
|
||||
26;4;
|
|
162
detectionprocessing.cpp
Normal file
@ -0,0 +1,162 @@
|
||||
#include "detectionprocessing.h"
|
||||
#include "controller.h"
|
||||
#include "colors.h"
|
||||
#include <QDebug>
|
||||
#include <QFileDialog>
|
||||
#include <QApplication>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
|
||||
using namespace std;
|
||||
using namespace cv;
|
||||
|
||||
DetectionProcessing::DetectionProcessing(QObject* parent)
|
||||
: Processing(parent) {}
|
||||
|
||||
void DetectionProcessing::init(Json::Value json_steps) {
|
||||
Processing::init(this->metaObject(), json_steps);
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_dilate(const Mat& input, int kernel_size,
|
||||
int iterations) {
|
||||
Mat out;
|
||||
dilate(input, out, getStructuringElement(MORPH_ELLIPSE,
|
||||
Size(kernel_size, kernel_size)), Point(-1, -1), iterations);
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_threshold(const Mat& input, int ignored,
|
||||
int ignored2) {
|
||||
Mat out;
|
||||
threshold(input, out, 127, 255, THRESH_BINARY);
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_invert(const Mat& input, int ignored, int ignored2) {
|
||||
Mat out = Scalar::all(255) - input;
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_invert_markers(const Mat& input, int ignored,
|
||||
int ignored2) {
|
||||
markers = Scalar::all(1) - markers;
|
||||
return markers;
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_thresh_color(const Mat& input, int lowerThreshold, int
|
||||
upperThreshold) {
|
||||
Mat hsv;
|
||||
cvtColor(input, hsv, COLOR_BGR2HSV);
|
||||
intermediate = Mat (input.size(), CV_8U);
|
||||
inRange(hsv, Scalar(lowerThreshold, 0, 0), Scalar(upperThreshold,
|
||||
255, 255), intermediate);
|
||||
return intermediate;
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_erode(const Mat& input, int kernel_size, int
|
||||
iterations) {
|
||||
Mat out;
|
||||
erode(input, out, getStructuringElement(MORPH_ELLIPSE,
|
||||
Size(kernel_size, kernel_size)), Point(-1, -1), iterations);
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_erode_intermediate(const Mat& input, int kernel_size,
|
||||
int iterations) {
|
||||
Mat out;
|
||||
erode(intermediate, out, getStructuringElement(MORPH_ELLIPSE,
|
||||
Size(kernel_size, kernel_size)), Point(-1, -1), iterations);
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_markBackground(const Mat& input, int ignored,
|
||||
int ignored2) {
|
||||
markers = Mat::zeros(input.size(), CV_32S);
|
||||
markers = Scalar::all(0);
|
||||
Mat in = input.clone();
|
||||
vector<vector<Point>> contours;
|
||||
findContours(in, contours, RETR_LIST, CHAIN_APPROX_NONE);
|
||||
for(int i = 0; i<contours.size(); i++) {
|
||||
drawContours(markers, contours, i, Scalar::all(1),
|
||||
CV_FILLED, 8);
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_watershed(const Mat& input, int ignored,
|
||||
int ignored2) {
|
||||
Mat in = input.clone();
|
||||
vector<vector<Point>> contours;
|
||||
findContours(in, contours, RETR_LIST, CHAIN_APPROX_SIMPLE);
|
||||
nut_count = 0;
|
||||
for(int i = 0; i<contours.size(); i++) {
|
||||
drawContours(markers, contours, i, Scalar::all(i+2),
|
||||
CV_FILLED, 8);
|
||||
nut_count++;
|
||||
}
|
||||
const Mat& src = *Controller::inst().getCurImg();
|
||||
watershed(src, markers);
|
||||
Mat wshed(markers.size(), CV_8UC3);
|
||||
// paint the watershed image
|
||||
for(int i = 0; i < markers.rows; i++ ) {
|
||||
for(int j = 0; j < markers.cols; j++ )
|
||||
{
|
||||
int index = markers.at<int>(i,j);
|
||||
if( index == -1 )
|
||||
wshed.at<Vec3b>(i,j) = Vec3b(255,255,255);
|
||||
else if( index <= 0 || index > nut_count )
|
||||
wshed.at<Vec3b>(i,j) = Vec3b(0,0,0);
|
||||
else
|
||||
wshed.at<Vec3b>(i,j) = colors[(index - 1) % colors.size()];
|
||||
}
|
||||
}
|
||||
wshed = wshed*0.5 + src*0.5;
|
||||
return wshed;
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_resize(const Mat& input, int new_length,
|
||||
int ignored) {
|
||||
Mat out;
|
||||
float fac = 1.0;
|
||||
if (input.rows > input.cols) {
|
||||
fac = (float) new_length/input.rows;
|
||||
}
|
||||
else {
|
||||
fac = (float) new_length/input.cols;
|
||||
}
|
||||
Size new_size((int)input.cols*fac, (int) input.rows*fac) ;
|
||||
resize(input, out, new_size, INTER_NEAREST);
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat DetectionProcessing::_resizeToOriginal(const Mat& input, int ignored,
|
||||
int ignored2) {
|
||||
Mat out;
|
||||
float fac = 1.0;
|
||||
if (input.rows > input.cols) {
|
||||
fac = (float) Controller::inst().getCurImg()->rows/input.rows;
|
||||
}
|
||||
else {
|
||||
fac = (float) Controller::inst().getCurImg()->cols/input.cols;
|
||||
}
|
||||
Size new_size((int)input.cols*fac, (int) input.rows*fac) ;
|
||||
resize(input, out, new_size, INTER_NEAREST);
|
||||
return out;
|
||||
}
|
||||
|
||||
const cv::Mat& DetectionProcessing::getResult() {
|
||||
return steps.back().getOutput();
|
||||
}
|
||||
|
||||
const cv::Mat& DetectionProcessing::getMarkers() {
|
||||
return markers;
|
||||
}
|
||||
|
||||
const cv::Mat* DetectionProcessing::getInput() {
|
||||
return Controller::inst().getCurImg();
|
||||
}
|
||||
|
||||
int DetectionProcessing::getNutCount() {
|
||||
return nut_count;
|
||||
}
|
43
detectionprocessing.h
Normal file
@ -0,0 +1,43 @@
|
||||
#ifndef DETECTIONPROCESSING_H
|
||||
#define DETECTIONPROCESSING_H
|
||||
|
||||
#include <QObject>
|
||||
#include "processing.h"
|
||||
#include <memory>
|
||||
|
||||
class DetectionProcessing : public Processing {
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DetectionProcessing(QObject *parent = 0);
|
||||
void init(Json::Value json_steps);
|
||||
const cv::Mat& getResult();
|
||||
const cv::Mat& getMarkers();
|
||||
int getNutCount();
|
||||
|
||||
// Processing methods
|
||||
public slots:
|
||||
cv::Mat _dilate(const cv::Mat& input , int kernel_size, int iterations);
|
||||
cv::Mat _threshold(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _thresh_color(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _erode(const cv::Mat& input , int kernel_size, int iterations);
|
||||
cv::Mat _erode_intermediate(const cv::Mat& input , int kernel_size, int iterations);
|
||||
cv::Mat _watershed(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _invert(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _invert_markers(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _resize(const cv::Mat& input, int new_length, int ignored);
|
||||
cv::Mat _resizeToOriginal(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _markBackground(const cv::Mat& input, int ignored, int ignored2);
|
||||
|
||||
private:
|
||||
const cv::Mat* getInput();
|
||||
|
||||
// Members
|
||||
private:
|
||||
cv::Mat markers;
|
||||
int nut_count;
|
||||
cv::Mat intermediate;
|
||||
};
|
||||
|
||||
#endif // DETECTIONPROCESSING_H
|
7
filenames.cpp
Normal file
@ -0,0 +1,7 @@
|
||||
#include "filenames.h"
|
||||
|
||||
const std::string FileNames::configName = "config.json";
|
||||
const std::string FileNames::inputName = "data/input.csv";
|
||||
const std::string FileNames::outputName = "data/output.csv";
|
||||
const std::string FileNames::imgPath = "data/imgs/";
|
||||
const std::string FileNames::neuralNetName = "data/nuts.net";
|
14
filenames.h
Normal file
@ -0,0 +1,14 @@
|
||||
#ifndef FILENAMES_H
|
||||
#define FILENAMES_H
|
||||
|
||||
#include <string>
|
||||
|
||||
struct FileNames {
|
||||
static const std::string configName;
|
||||
static const std::string inputName;
|
||||
static const std::string outputName;
|
||||
static const std::string imgPath;
|
||||
static const std::string neuralNetName;
|
||||
};
|
||||
|
||||
#endif // FILENAMES_H
|
14
main.cpp
Normal file
@ -0,0 +1,14 @@
|
||||
#include <mainwindow.h>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QApplication>
|
||||
#include <QMainWindow>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
QApplication app(argc, argv);
|
||||
MainWindow window;
|
||||
|
||||
window.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
264
mainwindow.cpp
Normal file
@ -0,0 +1,264 @@
|
||||
#include "mainwindow.h"
|
||||
#include "ui_mainwindow.h"
|
||||
#include <QDir>
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
|
||||
using namespace std;
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent) : controller(Controller::inst()),
|
||||
detProc(controller.getDetectionProcessing()),
|
||||
rateProc(controller.getRateProcessing()),
|
||||
QMainWindow(parent),
|
||||
ui(new Ui::MainWindow) {
|
||||
ui->setupUi(this);
|
||||
//QDir::setCurrent(qApp->applicationDirPath());
|
||||
connect(ui->loadButton, SIGNAL( clicked() ),
|
||||
this, SLOT( load() ));
|
||||
connect(ui->nextButton, SIGNAL( clicked() ),
|
||||
&detProc, SLOT( nextStep() ));
|
||||
connect(ui->previousButton, SIGNAL( clicked() ),
|
||||
&detProc, SLOT( previousStep() ));
|
||||
connect(ui->saveButton, SIGNAL( clicked() ),
|
||||
&detProc, SLOT( saveCur() ));
|
||||
connect(ui->saveConfigButton, SIGNAL( clicked() ),
|
||||
&controller, SLOT( saveDetectionConfig() ));
|
||||
connect(ui->captureButton, SIGNAL( clicked() ),
|
||||
this, SLOT( capture() ));
|
||||
connect(ui->resetButton, SIGNAL( clicked() ),
|
||||
&controller, SLOT( reset() ));
|
||||
captureShortcut = new QShortcut(QKeySequence("Return"),
|
||||
ui->liveTab);
|
||||
connect(captureShortcut, SIGNAL( activated() ),
|
||||
ui->captureButton, SLOT ( animateClick() ));
|
||||
captureShortcut2 = new QShortcut(QKeySequence("Space"),
|
||||
ui->liveTab);
|
||||
connect(captureShortcut2, SIGNAL( activated() ),
|
||||
ui->captureButton, SLOT ( animateClick() ));
|
||||
connect(&detProc, SIGNAL( newInputImage(const cv::Mat&) ),
|
||||
ui->inputView, SLOT( showImage(const cv::Mat&)));
|
||||
connect(&detProc, SIGNAL( newOutputImage(const cv::Mat&) ),
|
||||
ui->outputView, SLOT( showImage(const cv::Mat&)));
|
||||
connect(ui->param1SpinBox, SIGNAL ( valueChanged(int) ),
|
||||
&detProc, SLOT( setParam1(int) ));
|
||||
connect(ui->param2SpinBox, SIGNAL ( valueChanged(int) ),
|
||||
&detProc, SLOT( setParam2(int) ));
|
||||
connect(&detProc, SIGNAL( requestUpdate() ),
|
||||
this, SLOT( updateDetection() ));
|
||||
connect(&rateProc, SIGNAL( requestUpdate() ),
|
||||
this, SLOT( updateRating() ));
|
||||
connect(ui->rate_nextButton, SIGNAL( clicked() ),
|
||||
&rateProc, SLOT( nextStep() ));
|
||||
connect(ui->rate_previousButton, SIGNAL( clicked() ),
|
||||
&rateProc, SLOT( previousStep() ));
|
||||
connect(ui->rate_saveButton, SIGNAL( clicked() ),
|
||||
&rateProc, SLOT( saveCur() ));
|
||||
connect(ui->rate_saveConfigButton, SIGNAL( clicked() ),
|
||||
&controller, SLOT( saveRatingConfig() ));
|
||||
connect(&rateProc, SIGNAL( newInputImage(const cv::Mat&) ),
|
||||
ui->rate_inputView, SLOT( showImage(const cv::Mat&)));
|
||||
connect(&rateProc, SIGNAL( newOutputImage(const cv::Mat&) ),
|
||||
ui->rate_outputView, SLOT( showImage(const cv::Mat&)));
|
||||
connect(ui->rate_param1SpinBox, SIGNAL ( valueChanged(int) ),
|
||||
&rateProc, SLOT( setParam1(int) ));
|
||||
connect(ui->rate_param2SpinBox, SIGNAL ( valueChanged(int) ),
|
||||
&rateProc, SLOT( setParam2(int) ));
|
||||
connect(ui->configTabWidget, SIGNAL( currentChanged(int) ),
|
||||
this, SLOT( modeConfig(int) ));
|
||||
connect(ui->superTabWidget, SIGNAL( currentChanged(int) ),
|
||||
this, SLOT( mode(int) ));
|
||||
connect(ui->previousNut, SIGNAL( clicked() ),
|
||||
&controller, SLOT( previousNut() ));
|
||||
connect(ui->nextNut, SIGNAL( clicked() ),
|
||||
&controller, SLOT( nextNut() ));
|
||||
connect(&controller, SIGNAL( newNut(const cv::Mat&) ),
|
||||
ui->rate_inputView , SLOT( showImage(const cv::Mat&) ));
|
||||
connect(&controller, SIGNAL( newInputImage(const cv::Mat&) ),
|
||||
ui->liveView , SLOT( showImage(const cv::Mat&) ));
|
||||
connect(&controller, SIGNAL( newNutToRate(const cv::Mat&) ),
|
||||
ui->liveNut, SLOT( showImage(const cv::Mat&) ));
|
||||
connect(&controller, SIGNAL( message(std::string ) ),
|
||||
ui->liveNut, SLOT( showTextImage(std::string) ));
|
||||
connect(ui->rateButton1, SIGNAL(clicked()),
|
||||
this, SLOT( select1()));
|
||||
connect(ui->rateButton2, SIGNAL(clicked()),
|
||||
this, SLOT( select2()));
|
||||
connect(ui->rateButton3, SIGNAL(clicked()),
|
||||
this, SLOT( select3()));
|
||||
connect(ui->rateButton4, SIGNAL(clicked()),
|
||||
this, SLOT( select4()));
|
||||
connect(ui->rateButton5, SIGNAL(clicked()),
|
||||
this, SLOT( select5()));
|
||||
connect(&controller, SIGNAL( statusMessage(QString) ),
|
||||
ui->statusLabel, SLOT( setText(QString) ));
|
||||
connect(ui->neuralNetCheckBox, SIGNAL( stateChanged(int) ),
|
||||
&controller, SLOT( neuralNetChange(int) ));
|
||||
updateDetection();
|
||||
updateRating();
|
||||
updateN();
|
||||
}
|
||||
|
||||
void MainWindow::select1() {
|
||||
controller.currentRating(1);
|
||||
}
|
||||
|
||||
void MainWindow::select2() {
|
||||
controller.currentRating(2);
|
||||
}
|
||||
|
||||
void MainWindow::select3() {
|
||||
controller.currentRating(3);
|
||||
}
|
||||
|
||||
void MainWindow::select4() {
|
||||
controller.currentRating(4);
|
||||
}
|
||||
|
||||
void MainWindow::select5() {
|
||||
controller.currentRating(5);
|
||||
}
|
||||
|
||||
void MainWindow::load() {
|
||||
resetRateButtons();
|
||||
controller.openImage();
|
||||
updateN();
|
||||
}
|
||||
|
||||
void MainWindow::updateN() {
|
||||
ostringstream sstr;
|
||||
sstr << "N = " << to_string(controller.getN());
|
||||
ui->NLabel->setText(QString::fromStdString(sstr.str()));
|
||||
}
|
||||
|
||||
void MainWindow::capture() {
|
||||
resetRateButtons();
|
||||
ui->captureButton->setEnabled(false);
|
||||
ui->captureButton->blockSignals(true);
|
||||
captureShortcut->blockSignals(true);
|
||||
captureShortcut2->blockSignals(true);
|
||||
qApp->processEvents();
|
||||
controller.captureImage();
|
||||
ui->captureButton->setEnabled(true);
|
||||
captureShortcut->blockSignals(false);
|
||||
captureShortcut2->blockSignals(false);
|
||||
ui->captureButton->blockSignals(false);
|
||||
updateN();
|
||||
}
|
||||
|
||||
void MainWindow::resetRateButtons() {
|
||||
ui->rateButton1->setAutoExclusive(false);
|
||||
ui->rateButton2->setAutoExclusive(false);
|
||||
ui->rateButton3->setAutoExclusive(false);
|
||||
ui->rateButton4->setAutoExclusive(false);
|
||||
ui->rateButton5->setAutoExclusive(false);
|
||||
ui->rateButton1->setChecked(false);
|
||||
ui->rateButton2->setChecked(false);
|
||||
ui->rateButton3->setChecked(false);
|
||||
ui->rateButton4->setChecked(false);
|
||||
ui->rateButton5->setChecked(false);
|
||||
ui->rateButton1->setAutoExclusive(true);
|
||||
ui->rateButton2->setAutoExclusive(true);
|
||||
ui->rateButton3->setAutoExclusive(true);
|
||||
ui->rateButton4->setAutoExclusive(true);
|
||||
ui->rateButton5->setAutoExclusive(true);
|
||||
}
|
||||
|
||||
void MainWindow::updateDetection() {
|
||||
ui->progressBar->setValue(detProc.getProgress());
|
||||
ui->operationLabel->setText(QString::fromStdString("Aktuelle Operation: \""
|
||||
+ detProc.getStepName() + "\""));
|
||||
vector<Parameter> params = detProc.getCurParams();
|
||||
ui->param1Label->setText(QString::fromStdString("Parameter 1: \"" +
|
||||
params[0].name + "\""));
|
||||
ui->param2Label->setText(QString::fromStdString("Parameter 2: \"" +
|
||||
params[1].name + "\""));
|
||||
ui->param1SpinBox->blockSignals(true);
|
||||
ui->param2SpinBox->blockSignals(true);
|
||||
ui->param1Slider->setMinimum(params[0].lowerBound);
|
||||
ui->param1Slider->setMaximum(params[0].upperBound);
|
||||
ui->param1Slider->setValue(params[0].val);
|
||||
ui->param2Slider->setMinimum(params[1].lowerBound);
|
||||
ui->param2Slider->setMaximum(params[1].upperBound);
|
||||
ui->param2Slider->setValue(params[1].val);
|
||||
ui->param1SpinBox->setMinimum(params[0].lowerBound);
|
||||
ui->param1SpinBox->setMaximum(params[0].upperBound);
|
||||
ui->param1SpinBox->setValue(params[0].val);
|
||||
ui->param2SpinBox->setMinimum(params[1].lowerBound);
|
||||
ui->param2SpinBox->setMaximum(params[1].upperBound);
|
||||
ui->param2SpinBox->setValue(params[1].val);
|
||||
ui->param1SpinBox->blockSignals(false);
|
||||
ui->param2SpinBox->blockSignals(false);
|
||||
}
|
||||
|
||||
void MainWindow::updateRating() {
|
||||
ui->rate_progressBar->setValue(rateProc.getProgress());
|
||||
ui->rate_operationLabel->setText(QString::fromStdString(
|
||||
"Aktuelle Operation: \""
|
||||
+ rateProc.getStepName() + "\""));
|
||||
vector<Parameter> params = rateProc.getCurParams();
|
||||
ui->rate_param1Label->setText(QString::fromStdString("Parameter 1: \"" +
|
||||
params[0].name + "\""));
|
||||
ui->rate_param2Label->setText(QString::fromStdString("Parameter 2: \"" +
|
||||
params[1].name + "\""));
|
||||
ui->rate_param1SpinBox->blockSignals(true);
|
||||
ui->rate_param2SpinBox->blockSignals(true);
|
||||
ui->rate_param1Slider->setMinimum(params[0].lowerBound);
|
||||
ui->rate_param1Slider->setMaximum(params[0].upperBound);
|
||||
ui->rate_param1Slider->setValue(params[0].val);
|
||||
ui->rate_param2Slider->setMinimum(params[1].lowerBound);
|
||||
ui->rate_param2Slider->setMaximum(params[1].upperBound);
|
||||
ui->rate_param2Slider->setValue(params[1].val);
|
||||
ui->rate_param1SpinBox->setMinimum(params[0].lowerBound);
|
||||
ui->rate_param1SpinBox->setMaximum(params[0].upperBound);
|
||||
ui->rate_param1SpinBox->setValue(params[0].val);
|
||||
ui->rate_param2SpinBox->setMinimum(params[1].lowerBound);
|
||||
ui->rate_param2SpinBox->setMaximum(params[1].upperBound);
|
||||
ui->rate_param2SpinBox->setValue(params[1].val);
|
||||
ui->rate_param1SpinBox->blockSignals(false);
|
||||
ui->rate_param2SpinBox->blockSignals(false);
|
||||
ostringstream sstr;
|
||||
sstr << "Ergebnisse: ";
|
||||
for (auto&& result : rateProc.getResults()) {
|
||||
sstr << result.first << ": " << result.second << ", ";
|
||||
}
|
||||
ui->resultLabel->setText(QString::fromStdString(sstr.str()));
|
||||
}
|
||||
|
||||
void MainWindow::mode(int tabIndex) {
|
||||
if (tabIndex == 0) {
|
||||
detProc.setInteractive(false);
|
||||
rateProc.setInteractive(false);
|
||||
controller.process();
|
||||
}
|
||||
else if (tabIndex == 1) {
|
||||
modeConfig(ui->configTabWidget->currentIndex());
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::modeConfig(int tabIndex) {
|
||||
if (tabIndex == 0) {
|
||||
detProc.setInteractive(true);
|
||||
rateProc.setInteractive(false);
|
||||
updateDetection();
|
||||
detProc.start();
|
||||
}
|
||||
else if (tabIndex == 1) {
|
||||
detProc.setInteractive(false);
|
||||
rateProc.setInteractive(true);
|
||||
updateRating();
|
||||
controller.resetNuts();
|
||||
controller.extractNuts();
|
||||
rateProc.start(true);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent* event) {
|
||||
controller.saveData();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
delete ui;
|
||||
delete captureShortcut;
|
||||
delete captureShortcut2;
|
||||
}
|
53
mainwindow.h
Normal file
@ -0,0 +1,53 @@
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QShortcut>
|
||||
#include <QCloseEvent>
|
||||
#include <QTimer>
|
||||
#include "detectionprocessing.h"
|
||||
#include "rateprocessing.h"
|
||||
#include "controller.h"
|
||||
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = 0);
|
||||
~MainWindow();
|
||||
|
||||
private slots:
|
||||
void updateDetection();
|
||||
void updateRating();
|
||||
void modeConfig(int tabIndex);
|
||||
void mode(int tabIndex);
|
||||
void capture();
|
||||
void load();
|
||||
void select1();
|
||||
void select2();
|
||||
void select3();
|
||||
void select4();
|
||||
void select5();
|
||||
|
||||
private:
|
||||
void closeEvent(QCloseEvent* event);
|
||||
void resetRateButtons();
|
||||
void updateN();
|
||||
|
||||
// Members
|
||||
private:
|
||||
Ui::MainWindow* ui;
|
||||
Controller& controller;
|
||||
DetectionProcessing& detProc;
|
||||
RateProcessing& rateProc;
|
||||
QTimer* autoSaveTimer;
|
||||
QShortcut* captureShortcut;
|
||||
QShortcut* captureShortcut2;
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
691
mainwindow.ui
Normal file
@ -0,0 +1,691 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1097</width>
|
||||
<height>807</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1035</width>
|
||||
<height>560</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Haselnüsse</string>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="German" country="Germany"/>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="superTabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="liveTab">
|
||||
<attribute name="title">
|
||||
<string>Live</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_11">
|
||||
<item>
|
||||
<widget class="CVImageWidget" name="liveView" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="CVImageWidget" name="liveNut" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="rateGroupBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>3</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Geschmack</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_12">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="rateButton1">
|
||||
<property name="text">
|
||||
<string>&1</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>1</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="rateButton2">
|
||||
<property name="text">
|
||||
<string>&2</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>2</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="rateButton3">
|
||||
<property name="text">
|
||||
<string>&3</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>3</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="rateButton4">
|
||||
<property name="text">
|
||||
<string>&4</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>4</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="rateButton5">
|
||||
<property name="text">
|
||||
<string>&5</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>5</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Status:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="statusLabel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>9</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>... lade ...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="NLabel">
|
||||
<property name="text">
|
||||
<string>N = 0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<item>
|
||||
<widget class="QPushButton" name="loadButton">
|
||||
<property name="text">
|
||||
<string>Bild laden</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="captureButton">
|
||||
<property name="text">
|
||||
<string>Bild aufnehmen</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="resetButton">
|
||||
<property name="text">
|
||||
<string>Zurücksetzen</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="neuralNetCheckBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Benutze Neurales Netz</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="configTab">
|
||||
<attribute name="title">
|
||||
<string>Konfiguration</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="configTabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="detectTab">
|
||||
<attribute name="title">
|
||||
<string>Nüsse erkennen</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QPushButton" name="previousButton">
|
||||
<property name="text">
|
||||
<string>Vorheriger Schritt</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+S</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="nextButton">
|
||||
<property name="text">
|
||||
<string>Nächster Schritt</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Right</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="saveButton">
|
||||
<property name="text">
|
||||
<string>Speichern</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>24</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="operationLabel">
|
||||
<property name="text">
|
||||
<string>Aktuelle Operation:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_1">
|
||||
<property name="text">
|
||||
<string>Input:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Output:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="CVImageWidget" name="inputView" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="CVImageWidget" name="outputView" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="param1Label">
|
||||
<property name="text">
|
||||
<string>Parameter 1:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="param1Slider">
|
||||
<property name="maximum">
|
||||
<number>255</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="param1SpinBox">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>255</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="param2Label">
|
||||
<property name="text">
|
||||
<string>Parameter 2:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="param2Slider">
|
||||
<property name="maximum">
|
||||
<number>255</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="param2SpinBox">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>255</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>3</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="saveConfigButton">
|
||||
<property name="text">
|
||||
<string>Konfiguration speichern</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="rateTab">
|
||||
<attribute name="title">
|
||||
<string>Nüsse bewerten</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QPushButton" name="previousNut">
|
||||
<property name="text">
|
||||
<string>vorherige Nuss</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="nextNut">
|
||||
<property name="text">
|
||||
<string>nächste Nuss</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="rate_previousButton">
|
||||
<property name="text">
|
||||
<string>Vorheriger Schritt</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+S</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="rate_nextButton">
|
||||
<property name="text">
|
||||
<string>Nächster Schritt</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Right</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="rate_saveButton">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Speichern</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="rate_progressBar">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>24</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="rate_operationLabel">
|
||||
<property name="text">
|
||||
<string>Aktuelle Operation:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Input:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Output:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="CVImageWidget" name="rate_inputView" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="CVImageWidget" name="rate_outputView" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="rate_param1Label">
|
||||
<property name="text">
|
||||
<string>Parameter 1:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="rate_param1Slider">
|
||||
<property name="maximum">
|
||||
<number>255</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="rate_param1SpinBox">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>255</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="rate_param2Label">
|
||||
<property name="text">
|
||||
<string>Parameter 2:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="rate_param2Slider">
|
||||
<property name="maximum">
|
||||
<number>255</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="rate_param2SpinBox">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>255</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>3</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="rate_saveConfigButton">
|
||||
<property name="text">
|
||||
<string>Konfiguration speichern</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="resultLabel">
|
||||
<property name="text">
|
||||
<string>Ergebnis:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>CVImageWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>cvimagewidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>rate_param1Slider</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>rate_param1SpinBox</receiver>
|
||||
<slot>setValue(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>272</x>
|
||||
<y>729</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>408</x>
|
||||
<y>733</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>rate_param1SpinBox</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>rate_param1Slider</receiver>
|
||||
<slot>setValue(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>411</x>
|
||||
<y>718</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>226</x>
|
||||
<y>728</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>rate_param2SpinBox</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>rate_param2Slider</receiver>
|
||||
<slot>setValue(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>845</x>
|
||||
<y>726</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>797</x>
|
||||
<y>732</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>rate_param2Slider</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>rate_param2SpinBox</receiver>
|
||||
<slot>setValue(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>704</x>
|
||||
<y>726</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>840</x>
|
||||
<y>719</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
273
neuralnet.cpp
Normal file
@ -0,0 +1,273 @@
|
||||
#include "neuralnet.h"
|
||||
#include "controller.h"
|
||||
#include "filenames.h"
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <clocale>
|
||||
|
||||
using namespace std;
|
||||
using namespace FANN;
|
||||
|
||||
void NeuralNet::saveNut() {
|
||||
if (Controller::inst().getCurRating() == -1) {
|
||||
cerr << "Error: current rating -1 during saving." << endl;
|
||||
exit(1);
|
||||
}
|
||||
ostringstream input_line;
|
||||
input_line << current_index << ";";
|
||||
input_line.precision(numeric_limits<double>::max_digits10);
|
||||
const auto& results = Controller::inst().getRateProcessing().getResults();
|
||||
for (const auto& result : result_names) {
|
||||
auto pos = results.find(result);
|
||||
if (pos == results.end()) {
|
||||
cerr << "Could not find " << result << " in results." << endl;
|
||||
exit(1);
|
||||
}
|
||||
inputTrainData.push_back(pos->second);
|
||||
input_line << scientific << pos->second << ";";
|
||||
}
|
||||
ofstream input_file;
|
||||
input_file.open(FileNames::inputName, ios_base::app);
|
||||
input_file << input_line.str() << "\n";
|
||||
|
||||
outputTrainData.push_back(Controller::inst().getCurRating());
|
||||
ofstream output_file;
|
||||
output_file.open(FileNames::outputName, ios_base::app);
|
||||
output_file << current_index << ";";
|
||||
output_file << Controller::inst().getCurRating() << ";" << "\n";
|
||||
|
||||
ostringstream img_file;
|
||||
img_file << FileNames::imgPath
|
||||
<< "nut_" << to_string(current_index)
|
||||
<< ".jpg";
|
||||
imwrite(img_file.str(), *Controller::inst().getNutToRate());
|
||||
current_index++;
|
||||
if (current_index % 5 == 0) {
|
||||
trainAndSave();
|
||||
}
|
||||
}
|
||||
|
||||
void NeuralNet::trainAndSave() {
|
||||
setlocale(LC_ALL, "en_GB.UTF-8");
|
||||
getScalingFactors();
|
||||
scaleData();
|
||||
training_data data;
|
||||
data.set_train_data(scaledOutputTrainData.size(), result_names.size(),
|
||||
&scaledInputTrainData[0], 1, &scaledOutputTrainData[0]);
|
||||
net.train_on_data(data, 100000, 1000, 0.01);
|
||||
if (!net.save(FileNames::neuralNetName)) {
|
||||
cerr << "Error saving neural net:"
|
||||
<< net.get_errstr() << endl;
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void NeuralNet::scaleData() {
|
||||
scaledInputTrainData = vector<float>(inputTrainData.size());
|
||||
for(int i = 0; i<result_names.size(); i++) {
|
||||
float data_min = inputTrainData[i];
|
||||
float data_max = inputTrainData[i];
|
||||
for(int j = 0; j<outputTrainData.size(); j++) {
|
||||
|
||||
scaledInputTrainData[(j*result_names.size())+i] =
|
||||
(inputTrainData[(j*result_names.size())+i] -
|
||||
get<0>(scaling_input_data[i])) *
|
||||
get <1>(scaling_input_data[i]);
|
||||
}
|
||||
}
|
||||
scaledOutputTrainData = vector<float>(outputTrainData.size());
|
||||
for(int i = 0; i<outputTrainData.size(); i++) {
|
||||
scaledOutputTrainData[i] = (outputTrainData[i] - 1) * 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
void NeuralNet::getScalingFactors() {
|
||||
scaling_input_data = vector<tuple<float, float>>(result_names.size());
|
||||
for(int i = 0; i<result_names.size(); i++) {
|
||||
float data_min = inputTrainData[i];
|
||||
float data_max = inputTrainData[i];
|
||||
for(int j = 0; j<outputTrainData.size(); j++) {
|
||||
|
||||
data_min = min(data_min, inputTrainData[(j*result_names.size())+i]);
|
||||
data_max = max(data_max, inputTrainData[(j*result_names.size())+i]);
|
||||
}
|
||||
cout << "Input " << i << ": min, scaling_factor: " << data_min
|
||||
<< ", " << 1.0f/(data_max - data_min)
|
||||
<< " min: " << data_min
|
||||
<< " max: " << data_max << endl;
|
||||
scaling_input_data[i] = make_tuple(
|
||||
data_min, 1.0f/(data_max-data_min));
|
||||
}
|
||||
}
|
||||
|
||||
float NeuralNet::run(const map<string, double>& input) {
|
||||
float input_arr[input.size()];
|
||||
for (unsigned int i = 0; i<result_names.size(); i++) {
|
||||
auto pos = input.find(result_names[i]);
|
||||
if (pos == input.end()) {
|
||||
cerr << "Could not find " << result_names[i] << " in results."
|
||||
<< endl;
|
||||
exit(1);
|
||||
}
|
||||
if (scaling_input_data.size() > 0) {
|
||||
input_arr[i] =
|
||||
(pos->second - get<0>(scaling_input_data[i])) *
|
||||
get<1>(scaling_input_data[i]);
|
||||
}
|
||||
else {
|
||||
input_arr[i] = pos->second;
|
||||
}
|
||||
}
|
||||
float ret = 0.0f;
|
||||
if (use_neural_net) {
|
||||
float* output;
|
||||
output = net.run(input_arr);
|
||||
ret = output[0];
|
||||
}
|
||||
else {
|
||||
ret =
|
||||
0.25 * input_arr[0] +
|
||||
0.125* input_arr[1] +
|
||||
0.125* (1.0f/input_arr[2]) +
|
||||
0.25 * input_arr[3] +
|
||||
0.25 * input_arr[4];
|
||||
}
|
||||
return min(1.0f, max(ret, 0.0f));
|
||||
}
|
||||
|
||||
void NeuralNet::init(Json::Value jsonResults) {
|
||||
for(int i = 0; i<jsonResults.size(); i++) {
|
||||
result_names.push_back(jsonResults[i].asString());
|
||||
}
|
||||
if (filesExist()) {
|
||||
setlocale(LC_ALL, "en_GB.UTF-8");
|
||||
string inputLine;
|
||||
string outputLine;
|
||||
ifstream input_file(FileNames::inputName);
|
||||
ifstream output_file(FileNames::outputName);
|
||||
// check header
|
||||
getline(input_file, inputLine);
|
||||
ostringstream inputHeader;
|
||||
inputHeader << "nut_id" << ";";
|
||||
for (const auto& result : result_names) {
|
||||
inputHeader << result << ";";
|
||||
}
|
||||
if (inputLine != inputHeader.str()) {
|
||||
cerr << "Header for output CSV file invalid: " <<
|
||||
inputHeader.str() << endl;
|
||||
exit(1);
|
||||
}
|
||||
getline(output_file, outputLine);
|
||||
if (outputLine != "nut_id;score;") {
|
||||
cerr << "Header for output CSV file invalid: " <<
|
||||
outputLine << endl;
|
||||
exit(1);
|
||||
}
|
||||
while ( getline(input_file, inputLine)
|
||||
&& getline(output_file, outputLine)) {
|
||||
stringstream inputLineStream(inputLine);
|
||||
string cell;
|
||||
// read in index
|
||||
getline(inputLineStream, cell, ';');
|
||||
try {
|
||||
current_index = stoi(cell);
|
||||
}
|
||||
catch (invalid_argument e) {
|
||||
cerr << "Could not convert " << cell << " to int." << endl;
|
||||
exit(1);
|
||||
}
|
||||
while(getline(inputLineStream, cell, ';')) {
|
||||
double val;
|
||||
try {
|
||||
val= stof(cell);
|
||||
}
|
||||
catch (invalid_argument e) {
|
||||
cerr << "Could not convert " << cell << " to double."
|
||||
<< endl;
|
||||
exit(1);
|
||||
}
|
||||
if (val < 0) {
|
||||
cerr << "Error reading input data. Index " << current_index
|
||||
<< "has negative data." << endl;
|
||||
exit(1);
|
||||
}
|
||||
inputTrainData.push_back(val);
|
||||
}
|
||||
stringstream outputLineStream(outputLine);
|
||||
getline(outputLineStream, cell, ';');
|
||||
int output_index;
|
||||
try {
|
||||
output_index = stoi(cell);
|
||||
}
|
||||
catch (invalid_argument e) {
|
||||
cerr << "Could not convert " << cell << " to int." << endl;
|
||||
exit(1);
|
||||
}
|
||||
if (current_index != output_index) {
|
||||
cerr << "Error reading data: Expected index " << current_index
|
||||
<< " but got index " << output_index << endl;
|
||||
exit(1);
|
||||
}
|
||||
getline(outputLineStream, cell, ';');
|
||||
double val;
|
||||
try {
|
||||
val= stof(cell);
|
||||
}
|
||||
catch (invalid_argument e) {
|
||||
cerr << "Could not convert " << cell << " to double."
|
||||
<< endl;
|
||||
exit(1);
|
||||
}
|
||||
if (val < 0) {
|
||||
cerr << "Error reading output data. Index " << current_index
|
||||
<< "has negative data." << endl;
|
||||
exit(1);
|
||||
}
|
||||
outputTrainData.push_back(val);
|
||||
}
|
||||
current_index++;
|
||||
net.create_from_file(FileNames::neuralNetName);
|
||||
net.set_training_algorithm(TRAIN_BATCH);
|
||||
net.set_activation_function_hidden(SIGMOID);
|
||||
net.set_activation_function_output(LINEAR);
|
||||
getScalingFactors();
|
||||
trainAndSave();
|
||||
}
|
||||
else {
|
||||
createHeaders();
|
||||
int input_size = result_names.size();
|
||||
net.create_standard(3, input_size, input_size + 1, 1);
|
||||
net.set_training_algorithm(TRAIN_BATCH);
|
||||
net.set_activation_function_hidden(SIGMOID);
|
||||
net.set_activation_function_output(LINEAR);
|
||||
}
|
||||
cout << "current_index: " << current_index << endl;
|
||||
}
|
||||
|
||||
void NeuralNet::createHeaders() {
|
||||
ofstream input_file(FileNames::inputName);
|
||||
input_file << "nut_id" << ";";
|
||||
for (const auto& result : result_names) {
|
||||
input_file << result << ";";
|
||||
}
|
||||
input_file << "\n";
|
||||
ofstream output_file(FileNames::outputName);
|
||||
output_file << "nut_id" << ";";
|
||||
output_file << "score" << ";";
|
||||
output_file << "\n";
|
||||
}
|
||||
|
||||
bool NeuralNet::filesExist() {
|
||||
ifstream input_file(FileNames::inputName);
|
||||
ifstream output_file(FileNames::outputName);
|
||||
ifstream net_file(FileNames::neuralNetName);
|
||||
return input_file.good() && output_file.good() && net_file.good();
|
||||
}
|
||||
|
||||
int NeuralNet::getN() {
|
||||
return current_index;
|
||||
}
|
||||
|
||||
void NeuralNet::neuralNetEnabled(bool val) {
|
||||
use_neural_net = val;
|
||||
}
|
38
neuralnet.h
Normal file
@ -0,0 +1,38 @@
|
||||
#ifndef NEURALNET_H
|
||||
#define NEURALNET_H
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <json/json.h>
|
||||
#include <fann.h>
|
||||
#include <fann_cpp.h>
|
||||
|
||||
class NeuralNet {
|
||||
public:
|
||||
void saveNut();
|
||||
void init(Json::Value jsonResults);
|
||||
int getN();
|
||||
float run(const std::map<std::string, double>& input);
|
||||
void neuralNetEnabled(bool val);
|
||||
|
||||
private:
|
||||
void createHeaders();
|
||||
bool filesExist();
|
||||
void trainAndSave();
|
||||
void getScalingFactors();
|
||||
void scaleData();
|
||||
|
||||
private:
|
||||
int current_index = 0;
|
||||
std::vector<float> inputTrainData = {};
|
||||
std::vector<float> scaledInputTrainData = {};
|
||||
std::vector<float> outputTrainData = {};
|
||||
std::vector<float> scaledOutputTrainData = {};
|
||||
std::vector<std::string> result_names = {};
|
||||
FANN::neural_net net;
|
||||
// tuple of min and scaling factor for each input
|
||||
std::vector<std::tuple<float, float>> scaling_input_data = {};
|
||||
bool use_neural_net = true;
|
||||
};
|
||||
|
||||
#endif // NEURALNET_H
|
183
processing.cpp
Normal file
@ -0,0 +1,183 @@
|
||||
#include "processing.h"
|
||||
#include "controller.h"
|
||||
#include <QDebug>
|
||||
#include <QFileDialog>
|
||||
#include <QApplication>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
|
||||
using namespace std;
|
||||
using namespace cv;
|
||||
|
||||
|
||||
Processing::Processing(QObject *parent) : QObject(parent) {}
|
||||
|
||||
void Processing::init(const QMetaObject* metaObj, Json::Value json_steps) {
|
||||
for(int i = 0; i<json_steps.size(); i++) {
|
||||
string json_name = json_steps[i].get("name", "").asString();
|
||||
string json_op_name = json_steps[i].get("operation", "").asString();
|
||||
QMetaMethod operation;
|
||||
bool found = false;
|
||||
for (int i = 0; i<metaObj->methodCount(); i++) {
|
||||
QMetaMethod method = metaObj->method(i);
|
||||
if (method.name().toStdString() == json_op_name) {
|
||||
operation = method;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
cerr << "Processing method " << json_op_name <<
|
||||
" not found.\n";
|
||||
exit(1);
|
||||
}
|
||||
Json::Value json_param1 = json_steps[i]["param1"];
|
||||
Parameter param1;
|
||||
param1.val = json_param1.get("default", 0).asInt();
|
||||
param1.lowerBound = json_param1.get("lowerBound", 0).asInt();
|
||||
param1.upperBound = json_param1.get("upperBound", 0).asInt();
|
||||
param1.name = json_param1.get("name", "").asString();
|
||||
param1.odd = json_param1.get("odd", false).asBool();
|
||||
Json::Value json_param2 = json_steps[i]["param2"];
|
||||
Parameter param2;
|
||||
param2.val = json_param2.get("default", 0).asInt();
|
||||
param2.lowerBound = json_param2.get("lowerBound", 0).asInt();
|
||||
param2.upperBound = json_param2.get("upperBound", 0).asInt();
|
||||
param2.name = json_param2.get("name", "").asString();
|
||||
param2.odd = json_param2.get("odd", false).asBool();
|
||||
vector<Parameter> params = {param1, param2};
|
||||
InputType type;
|
||||
if (json_steps[i]["input"] == "previous") {
|
||||
type = previous;
|
||||
}
|
||||
else if (json_steps[i]["input"] == "color") {
|
||||
type = color;
|
||||
}
|
||||
ProcessingStep step(this, json_name, operation, params, type);
|
||||
steps.push_back(step);
|
||||
}
|
||||
}
|
||||
|
||||
void Processing::start(bool force) {
|
||||
if (!steps[current_step].hasInputData()) {
|
||||
return;
|
||||
}
|
||||
if (interactive) {
|
||||
emit newInputImage(steps[current_step].getInput());
|
||||
}
|
||||
if (steps[current_step].getOutput().empty() || force) {
|
||||
processImage();
|
||||
}
|
||||
else {
|
||||
emit newOutputImage(steps[current_step].getOutput());
|
||||
}
|
||||
}
|
||||
|
||||
void Processing::reset() {
|
||||
current_step = 0;
|
||||
for (auto&& step : steps) {
|
||||
step.reset();
|
||||
}
|
||||
InputType type = steps[0].getInputType();
|
||||
if (type == color) {
|
||||
steps[0].setInput(Controller::inst().getCurImg());
|
||||
}
|
||||
}
|
||||
|
||||
void Processing::processImage() {
|
||||
steps[current_step].execute();
|
||||
if (interactive) {
|
||||
emit newOutputImage(steps[current_step].getOutput());
|
||||
}
|
||||
}
|
||||
|
||||
void Processing::setParam1(int val) {
|
||||
steps[current_step].setParam1(val);
|
||||
processImage();
|
||||
}
|
||||
|
||||
void Processing::setParam2(int val) {
|
||||
steps[current_step].setParam2(val);
|
||||
processImage();
|
||||
}
|
||||
|
||||
void Processing::nextStep() {
|
||||
current_step = min((int)steps.size() - 1, current_step+1);
|
||||
InputType type = steps[current_step].getInputType();
|
||||
if (type == previous) {
|
||||
steps[current_step].setInput(&steps[current_step -
|
||||
1].getOutput());
|
||||
if (interactive) {
|
||||
emit newInputImage(steps[current_step].getInput());
|
||||
}
|
||||
}
|
||||
else if (type == color) {
|
||||
steps[current_step].setInput(getInput());
|
||||
if (interactive && steps[current_step].hasInputData()) {
|
||||
emit newInputImage(steps[current_step].getInput());
|
||||
}
|
||||
}
|
||||
if (interactive) {
|
||||
emit requestUpdate();
|
||||
}
|
||||
if (steps[current_step].hasInputData()) {
|
||||
processImage();
|
||||
}
|
||||
else {
|
||||
cerr << "No input data for step: "
|
||||
+ steps[current_step].getStepName() << endl;
|
||||
}
|
||||
}
|
||||
|
||||
void Processing::previousStep() {
|
||||
current_step = max(0, current_step-1);
|
||||
if (interactive) {
|
||||
emit requestUpdate();
|
||||
if (steps[current_step].hasData()) {
|
||||
emit newInputImage(steps[current_step].getInput());
|
||||
emit newOutputImage(steps[current_step].getOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Processing::getProgress() {
|
||||
double prog = (double)(current_step+1)/steps.size();
|
||||
return ceil(prog*100);
|
||||
}
|
||||
|
||||
string Processing::getStepName() {
|
||||
return steps[current_step].getStepName();
|
||||
}
|
||||
|
||||
vector<Parameter> Processing::getCurParams() {
|
||||
return steps[current_step].getParams();
|
||||
}
|
||||
|
||||
void Processing::saveCur() {
|
||||
steps[current_step].save();
|
||||
}
|
||||
|
||||
void Processing::setInteractive(bool val) {
|
||||
interactive = val;
|
||||
}
|
||||
|
||||
bool Processing::isInteractive() {
|
||||
return interactive;
|
||||
}
|
||||
|
||||
int Processing::getCurrentStep() {
|
||||
return current_step;
|
||||
}
|
||||
|
||||
bool Processing::process() {
|
||||
if (! steps[0].hasInputData()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// also do step 0
|
||||
current_step = -1;
|
||||
for (unsigned int i = 0; i<steps.size(); i++) {
|
||||
nextStep();
|
||||
}
|
||||
return true;
|
||||
}
|
50
processing.h
Normal file
@ -0,0 +1,50 @@
|
||||
#ifndef PROCESSING_H
|
||||
#define PROCESSING_H
|
||||
#include <QObject>
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <json/json.h>
|
||||
|
||||
#include "processingstep.h"
|
||||
|
||||
class Processing : public QObject {
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Processing(QObject *parent = 0);
|
||||
std::vector<Parameter> getCurParams();
|
||||
std::string getStepName();
|
||||
int getProgress();
|
||||
void setInteractive(bool val);
|
||||
bool isInteractive();
|
||||
void start(bool force = false);
|
||||
void reset();
|
||||
int getCurrentStep();
|
||||
bool process();
|
||||
|
||||
signals:
|
||||
void newInputImage(const cv::Mat& newImage);
|
||||
void newOutputImage(const cv::Mat& newImage);
|
||||
void requestUpdate();
|
||||
|
||||
public slots:
|
||||
void setParam1(int val);
|
||||
void setParam2(int val);
|
||||
void nextStep();
|
||||
void previousStep();
|
||||
void saveCur();
|
||||
void processImage();
|
||||
|
||||
protected:
|
||||
virtual const cv::Mat* getInput() = 0;
|
||||
|
||||
// Members
|
||||
protected:
|
||||
std::vector<ProcessingStep> steps {};
|
||||
int current_step = 0;
|
||||
Json::Value config;
|
||||
void init(const QMetaObject* metaObj, Json::Value json_steps);
|
||||
bool interactive = false;
|
||||
};
|
||||
|
||||
#endif // PROCESSING_H
|
100
processingstep.h
Normal file
@ -0,0 +1,100 @@
|
||||
#ifndef PROCESSINGSTEP_H
|
||||
#define PROCESSINGSTEP_H
|
||||
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <functional>
|
||||
#include <QMetaMethod>
|
||||
#include <QObject>
|
||||
|
||||
struct Parameter {
|
||||
int val;
|
||||
int lowerBound;
|
||||
int upperBound;
|
||||
std::string name;
|
||||
bool odd;
|
||||
};
|
||||
|
||||
enum InputType {
|
||||
previous,
|
||||
color
|
||||
};
|
||||
|
||||
class ProcessingStep {
|
||||
|
||||
public:
|
||||
ProcessingStep(QObject* processing,
|
||||
std::string stepName,
|
||||
QMetaMethod operation,
|
||||
std::vector<Parameter> params,
|
||||
InputType inputType
|
||||
) {
|
||||
this->processing = processing;
|
||||
this->stepName = stepName;
|
||||
this->operation = operation;
|
||||
this->params = params;
|
||||
this->inputType = inputType;
|
||||
}
|
||||
|
||||
std::vector<Parameter> getParams() { return params; }
|
||||
std::string getStepName() { return stepName; }
|
||||
void setParam1 (int val) {
|
||||
if (params[0].odd && val % 2 == 0) {
|
||||
val--;
|
||||
}
|
||||
params[0].val = val;
|
||||
}
|
||||
|
||||
int getParam1() { return params[0].val; }
|
||||
|
||||
int getParam2() { return params[1].val; }
|
||||
|
||||
void setParam2 (int val) {
|
||||
if (params[1].odd && val % 2 == 0) {
|
||||
val--;
|
||||
}
|
||||
params[1].val = val;
|
||||
}
|
||||
void execute() {
|
||||
if (input->empty()) {
|
||||
std::cerr << "Input empty for processing step: " << stepName << std::endl;
|
||||
return;
|
||||
}
|
||||
operation.invoke(processing,
|
||||
Q_RETURN_ARG(cv::Mat, output),
|
||||
Q_ARG(const cv::Mat&, *input),
|
||||
Q_ARG(int, params[0].val),
|
||||
Q_ARG(int, params[1].val)
|
||||
);
|
||||
}
|
||||
void save() {
|
||||
imwrite(stepName + ".jpg", output);
|
||||
}
|
||||
|
||||
const cv::Mat& getOutput() { return output; }
|
||||
const cv::Mat& getInput() { return *input; }
|
||||
|
||||
void setInput(const cv::Mat* input) { this->input = input; }
|
||||
|
||||
InputType getInputType() { return inputType; }
|
||||
|
||||
bool hasInputData() { return input != nullptr && !input->empty(); }
|
||||
|
||||
bool hasData() { return input != nullptr && !input->empty()
|
||||
&& !output.empty(); }
|
||||
|
||||
void reset() {
|
||||
input = nullptr;
|
||||
output.release();
|
||||
}
|
||||
|
||||
private:
|
||||
const cv::Mat* input = nullptr;
|
||||
cv::Mat output;
|
||||
std::string stepName;
|
||||
QMetaMethod operation;
|
||||
std::vector<Parameter> params;
|
||||
InputType inputType;
|
||||
QObject* processing;
|
||||
};
|
||||
|
||||
#endif
|
311
rateprocessing.cpp
Normal file
@ -0,0 +1,311 @@
|
||||
#include "detectionprocessing.h"
|
||||
#include "controller.h"
|
||||
#include <QDebug>
|
||||
#include <QFileDialog>
|
||||
#include <QApplication>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <opencv2/highgui.hpp>
|
||||
|
||||
using namespace std;
|
||||
using namespace cv;
|
||||
|
||||
RateProcessing::RateProcessing(QObject* parent)
|
||||
: Processing(parent) {}
|
||||
|
||||
void RateProcessing::init(Json::Value json_steps) {
|
||||
Processing::init(this->metaObject(), json_steps);
|
||||
}
|
||||
|
||||
|
||||
Mat RateProcessing::_dilate(const Mat& input, int kernel_size,
|
||||
int iterations) {
|
||||
Mat out;
|
||||
dilate(input, out, getStructuringElement(MORPH_ELLIPSE,
|
||||
Size(kernel_size, kernel_size)), Point(-1, -1), iterations);
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat RateProcessing::_threshold(const Mat& input, int ignored,
|
||||
int ignored2) {
|
||||
Mat gray;
|
||||
cvtColor(input, gray, COLOR_BGR2GRAY);
|
||||
Mat out;
|
||||
threshold(gray, out, 1, 255, THRESH_BINARY);
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat RateProcessing::_thresh_otsu(const Mat& input, int ignored,
|
||||
int ignored2) {
|
||||
Mat gray;
|
||||
cvtColor(input, gray, COLOR_BGR2GRAY);
|
||||
Mat out;
|
||||
threshold(gray, out, 0, 255, THRESH_BINARY | THRESH_OTSU);
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat RateProcessing::_invert(const Mat& input, int ignored, int ignored2) {
|
||||
Mat out = Scalar::all(255) - input;
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat RateProcessing::_thresh_color(const Mat& input, int lowerThreshold, int
|
||||
upperThreshold) {
|
||||
Mat hsv;
|
||||
cvtColor(input, hsv, COLOR_BGR2HSV);
|
||||
Mat out(input.size(), CV_8U);
|
||||
inRange(hsv, Scalar(lowerThreshold, 0, 0), Scalar(upperThreshold,
|
||||
255, 255), out);
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat RateProcessing::_mean_color(const Mat& input, int ignored, int ignored2) {
|
||||
Mat hsv_nut;
|
||||
cvtColor(*getInput(), hsv_nut, CV_BGR2HSV);
|
||||
Scalar res = mean(hsv_nut, input);
|
||||
results["avg_h_val"] = res[0];
|
||||
if (isInteractive()) {
|
||||
emit requestUpdate();
|
||||
}
|
||||
return hsv_nut;
|
||||
}
|
||||
|
||||
Mat RateProcessing::_erode(const Mat& input, int kernel_size, int
|
||||
iterations) {
|
||||
Mat out;
|
||||
erode(input, out, getStructuringElement(MORPH_ELLIPSE,
|
||||
Size(kernel_size, kernel_size)), Point(-1, -1), iterations);
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat RateProcessing::_hough(const Mat& input, int canny_1, int canny_2) {
|
||||
vector<Vec3f> circles;
|
||||
Mat gray;
|
||||
cvtColor(input, gray, COLOR_BGR2GRAY);
|
||||
HoughCircles(gray, circles, HOUGH_GRADIENT, 1, input.rows/4, canny_1,
|
||||
canny_2);
|
||||
Mat out;
|
||||
input.copyTo(out);
|
||||
for (int i = 0; i<circles.size(); i++) {
|
||||
Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
|
||||
int radius = cvRound(circles[i][2]);
|
||||
circle(out, center, 3, Scalar(0, 255, 0), -1, 8, 0);
|
||||
circle(out, center, radius, Scalar(0, 0, 255), 3, 8, 0);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat RateProcessing::_find_contours_resized(const Mat& input, int max_length,
|
||||
int ignored2) {
|
||||
vector<vector<Point>> contours;
|
||||
Mat in;
|
||||
float fac = 1.0;
|
||||
if (input.rows > input.cols) {
|
||||
fac = (float) max_length/input.rows;
|
||||
}
|
||||
else {
|
||||
fac = (float) max_length/input.cols;
|
||||
}
|
||||
Size new_size((int)input.cols*fac, (int) input.rows*fac) ;
|
||||
resize(input, in, new_size);
|
||||
findContours(in, contours, RETR_LIST, CHAIN_APPROX_TC89_KCOS);
|
||||
Mat out;
|
||||
if (isInteractive()) {
|
||||
out = Mat::zeros(in.size(), CV_8UC3);
|
||||
for(int i = 0; i < contours.size(); i++) {
|
||||
drawContours(out, contours, i, Scalar::all(255), 1, 8);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat RateProcessing::_find_contours(const Mat& input, int ignored,
|
||||
int ignored2) {
|
||||
Mat in = input.clone();
|
||||
vector<vector<Point>> contours;
|
||||
Mat out;
|
||||
findContours(in, contours, RETR_LIST, CHAIN_APPROX_NONE);
|
||||
if (isInteractive()) {
|
||||
out = Mat::zeros(input.size(), CV_8UC3);
|
||||
for(int i = 0; i < contours.size(); i++) {
|
||||
drawContours(out, contours, i, Scalar::all(255), 1, 8);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Mat RateProcessing::_fit_ellipse(const Mat& input, int ignored, int ignored2) {
|
||||
Mat in = input.clone();
|
||||
//vector<vector<Point>> contours;
|
||||
contours = {};
|
||||
Mat out;
|
||||
findContours(in, contours, RETR_LIST, CHAIN_APPROX_NONE);
|
||||
if (isInteractive()) {
|
||||
out = Mat::zeros(input.size(), CV_8UC3);
|
||||
}
|
||||
vector<RotatedRect> boxes = {};
|
||||
for(int i = 0; i < contours.size(); i++) {
|
||||
int count = contours[i].size();
|
||||
if( count < 6 )
|
||||
continue;
|
||||
Mat pointsf;
|
||||
Mat(contours[i]).convertTo(pointsf, CV_32F);
|
||||
RotatedRect box = fitEllipse(pointsf);
|
||||
if( MAX(box.size.width, box.size.height) >
|
||||
MIN(box.size.width, box.size.height)*30 )
|
||||
continue;
|
||||
boxes.push_back(box);
|
||||
if (isInteractive()) {
|
||||
drawContours(out, contours, i, Scalar::all(255), 1, 8);
|
||||
ellipse(out, box, Scalar(0,0,255));
|
||||
Point2f vtx[4];
|
||||
box.points(vtx);
|
||||
for( int j = 0; j < 4; j++ ) {
|
||||
line(out, vtx[j], vtx[(j+1)%4], Scalar(0,255,0), 1, LINE_AA);
|
||||
}
|
||||
}
|
||||
}
|
||||
RotatedRect& max_box = boxes[0];
|
||||
int max_area = 0;
|
||||
for (auto&& box : boxes) {
|
||||
if ((box.size.width * box.size.height) > max_area) {
|
||||
max_box = box;
|
||||
max_area = box.size.width * box.size.height;
|
||||
}
|
||||
}
|
||||
results["size_ratio"] = max_box.size.width/max_box.size.height;
|
||||
results["size_diagonal"] =
|
||||
sqrt(max_box.size.height * max_box.size.height +
|
||||
max_box.size.height * max_box.size.height)
|
||||
/ static_cast<float>(steps[current_step].getParam1());
|
||||
if (isInteractive()) {
|
||||
emit requestUpdate();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Mat RateProcessing::_rate_spots(const Mat& input, int ignored,
|
||||
int ignored2) {
|
||||
Mat in = input.clone();
|
||||
vector<vector<Point>> contours;
|
||||
Mat out;
|
||||
findContours(in, contours, RETR_LIST, CHAIN_APPROX_NONE);
|
||||
if (isInteractive()) {
|
||||
out = Mat::zeros(input.size(), CV_8UC3);
|
||||
}
|
||||
vector<RotatedRect> boxes = {};
|
||||
// image and mask of spots
|
||||
vector<tuple<Mat, Mat>> spots = {};
|
||||
for(int i = 0; i < contours.size(); i++) {
|
||||
int count = contours[i].size();
|
||||
if( count < 6 )
|
||||
continue;
|
||||
Mat pointsf;
|
||||
Mat(contours[i]).convertTo(pointsf, CV_32F);
|
||||
RotatedRect box = fitEllipse(pointsf);
|
||||
if( MAX(box.size.width, box.size.height) >
|
||||
MIN(box.size.width, box.size.height)*30 )
|
||||
continue;
|
||||
boxes.push_back(box);
|
||||
if (isInteractive()) {
|
||||
drawContours(out, contours, i, Scalar::all(255), 1, 8);
|
||||
ellipse(out, box, Scalar(0,0,255));
|
||||
Point2f vtx[4];
|
||||
box.points(vtx);
|
||||
for( int j = 0; j < 4; j++ ) {
|
||||
line(out, vtx[j], vtx[(j+1)%4], Scalar(0,255,0), 1, LINE_AA);
|
||||
}
|
||||
}
|
||||
// extract spot from image
|
||||
Rect roi = boundingRect(contours[i]);
|
||||
Mat mask = Mat::zeros(input.size(), CV_8UC1);
|
||||
drawContours(mask, contours, i, Scalar(255), CV_FILLED);
|
||||
Mat contourRegion;
|
||||
Mat imageROI;
|
||||
getInput()->copyTo(imageROI, mask);
|
||||
contourRegion = imageROI(roi);
|
||||
Mat maskRegion = mask(roi);
|
||||
spots.push_back(make_tuple(contourRegion, maskRegion));
|
||||
}
|
||||
|
||||
// find largest ellipse to ignore it, because its no spot but the nut itself
|
||||
int max_box_index = -1;
|
||||
int max_area = 0;
|
||||
for (unsigned int i = 0; i<boxes.size(); i++) {
|
||||
if ((boxes[i].size.width * boxes[i].size.height) > max_area) {
|
||||
max_box_index = i;
|
||||
max_area = boxes[i].size.width * boxes[i].size.height;
|
||||
}
|
||||
}
|
||||
double box_avg_size = 0.0f;
|
||||
for (unsigned int i = 0; i<boxes.size(); i++) {
|
||||
if (i == max_box_index) {
|
||||
continue;
|
||||
}
|
||||
box_avg_size +=
|
||||
sqrt(boxes[i].size.width * boxes[i].size.width +
|
||||
boxes[i].size.height * boxes[i].size.height);
|
||||
}
|
||||
if (boxes.size() > 1) {
|
||||
box_avg_size /= static_cast<double>(boxes.size() - 1);
|
||||
}
|
||||
else {
|
||||
box_avg_size = 0.0f;
|
||||
}
|
||||
results["spot_avg_size"] = box_avg_size/steps[current_step].getParam1();
|
||||
double spot_avg_h_val = 0.0f;
|
||||
for (unsigned int i = 0; i<spots.size(); i++) {
|
||||
if (i == max_box_index) {
|
||||
continue;
|
||||
}
|
||||
Mat hsv_spot;
|
||||
cvtColor(get<0>(spots[i]), hsv_spot, CV_BGR2HSV);
|
||||
Scalar res = mean(hsv_spot, get<1>(spots[i]));
|
||||
spot_avg_h_val += res[0];
|
||||
}
|
||||
if (spots.size() > 1) {
|
||||
spot_avg_h_val /= static_cast<double>(spots.size() - 1);
|
||||
}
|
||||
else {
|
||||
spot_avg_h_val = 0.0f;
|
||||
}
|
||||
results["spot_avg_h_val"] = spot_avg_h_val;
|
||||
if (isInteractive()) {
|
||||
emit requestUpdate();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void RateProcessing::reset() {
|
||||
current_step = 0;
|
||||
InputType type = steps[0].getInputType();
|
||||
if (type == color) {
|
||||
steps[0].setInput(Controller::inst().getCurNut());
|
||||
}
|
||||
results.clear();
|
||||
rateCurNut = true;
|
||||
}
|
||||
|
||||
map<string, double> RateProcessing::getResults() {
|
||||
return results;
|
||||
}
|
||||
|
||||
const cv::Mat* RateProcessing::getInput() {
|
||||
if (rateCurNut) {
|
||||
return Controller::inst().getCurNut();
|
||||
}
|
||||
else {
|
||||
return Controller::inst().getNutToRate();
|
||||
}
|
||||
}
|
||||
|
||||
const vector<vector<Point>>& RateProcessing::getContours() {
|
||||
return contours;
|
||||
}
|
||||
|
||||
void RateProcessing::setRateCurNut(bool val) {
|
||||
rateCurNut = val;
|
||||
}
|
46
rateprocessing.h
Normal file
@ -0,0 +1,46 @@
|
||||
#ifndef RATEPROCESSING_H
|
||||
#define RATEPROCESSING_H
|
||||
|
||||
#include <QObject>
|
||||
#include "processing.h"
|
||||
#include <memory>
|
||||
#include <map>
|
||||
|
||||
class RateProcessing : public Processing {
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
RateProcessing(QObject *parent = 0);
|
||||
void init(Json::Value json_steps);
|
||||
void reset();
|
||||
std::map<std::string, double> getResults();
|
||||
const std::vector<std::vector<cv::Point>>& getContours();
|
||||
void setRateCurNut(bool val);
|
||||
|
||||
// Processing methods
|
||||
public slots:
|
||||
cv::Mat _dilate(const cv::Mat& input , int kernel_size, int iterations);
|
||||
cv::Mat _threshold(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _thresh_otsu(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _thresh_color(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _erode(const cv::Mat& input , int kernel_size, int iterations);
|
||||
cv::Mat _invert(const cv::Mat& input, int iter, int ignored);
|
||||
cv::Mat _hough(const cv::Mat& input, int canny_1, int canny_2);
|
||||
cv::Mat _fit_ellipse(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _rate_spots(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _find_contours(const cv::Mat& input, int ignored, int ignored2);
|
||||
cv::Mat _find_contours_resized(const cv::Mat& input, int max_length, int ignored2);
|
||||
cv::Mat _mean_color(const cv::Mat& input, int ignored, int ignored2);
|
||||
|
||||
private:
|
||||
const cv::Mat* getInput();
|
||||
|
||||
// Members
|
||||
private:
|
||||
bool rateCurNut = true;
|
||||
std::map<std::string, double> results;
|
||||
std::vector<std::vector<cv::Point>> contours;
|
||||
};
|
||||
|
||||
#endif // RATEPROCESSING_H
|
4
test_data/.directory
Normal file
@ -0,0 +1,4 @@
|
||||
[Dolphin]
|
||||
PreviewsShown=true
|
||||
Timestamp=2016,11,29,0,38,21
|
||||
Version=3
|
BIN
test_data/IMG_20170117_012527.jpg
Normal file
After Width: | Height: | Size: 5.4 MiB |