commit 2dfee5edbe987155f6b575c29848526c6c52748e Author: Evgeniy Date: Thu May 16 15:41:59 2024 +0700 Base diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebfe8a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +#!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + + + +# My .gitignore: +# Eclipse data: + +bin/ + +.settings/ + +### Java ### +*.class + +# Package Files # +*.jar +*.war +*.ear + +# Log file + +*.log + + +!gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8049c68 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mask-color-descriptors/build.gradle b/mask-color-descriptors/build.gradle new file mode 100644 index 0000000..3b8efaa --- /dev/null +++ b/mask-color-descriptors/build.gradle @@ -0,0 +1,49 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java library project to get you started. + * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle + * User Manual available at https://docs.gradle.org/7.5/userguide/building_java_projects.html + */ + +plugins { + // Apply the java-library plugin for API and implementation separation. + id 'java-library' +} + +compileJava.options.encoding = 'UTF-8' +compileTestJava.options.encoding = 'UTF-8' + + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + // Use JUnit Jupiter for testing. + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' + + // This dependency is exported to consumers, that is to say found on their compile classpath. + implementation 'org.apache.commons:commons-math3:3.6.1' + + // This dependency is used internally, and not exposed to consumers on their own compile classpath. + //implementation 'com.google.guava:guava:31.0.1-jre' + + // https://mvnrepository.com/artifact/org.knowm.xchart/xchart + implementation 'org.knowm.xchart:xchart:3.5.4' + + + //testImplementation("org.openpnp:opencv:3.4.2-0") + implementation("org.openpnp:opencv:3.4.2-0") + + //testImplementation("org.opencv:opencv:4.9.0") + //implementation("org.opencv:opencv:4.9.0") + + implementation project(":utils") +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/Library.java b/mask-color-descriptors/src/main/java/ru/delkom07/Library.java new file mode 100644 index 0000000..d7323b9 --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/Library.java @@ -0,0 +1,10 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package ru.delkom07; + +public class Library { + public boolean someLibraryMethod() { + return true; + } +} diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/ColorDescriptorsDemo.java b/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/ColorDescriptorsDemo.java new file mode 100644 index 0000000..f80285d --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/ColorDescriptorsDemo.java @@ -0,0 +1,214 @@ +package ru.delkom07.improc.color; +import java.awt.Color; +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.knowm.xchart.BitmapEncoder; +import org.knowm.xchart.BitmapEncoder.BitmapFormat; +import org.knowm.xchart.CategoryChart; +import org.knowm.xchart.CategoryChartBuilder; +import org.knowm.xchart.PieChart; +import org.knowm.xchart.PieChartBuilder; +import org.knowm.xchart.SwingWrapper; +import org.knowm.xchart.internal.chartpart.Chart; +import org.knowm.xchart.style.Styler.LegendPosition; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.imgcodecs.Imgcodecs; + +import ru.delkom07.improc.color.descriptors.ColorLayoutDescriptor; +import ru.delkom07.improc.color.descriptors.DominantColor; +import ru.delkom07.improc.color.descriptors.DominantColorDescriptor; +import ru.delkom07.improc.color.descriptors.GlobalColorHistogram; +import ru.delkom07.improc.color.descriptors.MeanColorDescriptor; +import ru.delkom07.improc.color.descriptors.SimpleHistograms; +import ru.delkom07.util.Pair; + + +public class ColorDescriptorsDemo { + + public static void main(String[] args) throws IOException { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + + if(0 == args.length) { + printHelp(); + System.exit(-1); + } + + File imgFile = new File(args[0]); + Mat img = Imgcodecs.imread(imgFile.getCanonicalPath()); + + ColorDescriptorsDemo cd = new ColorDescriptorsDemo(); + + Chart mcChart = cd.calculateMeanColorDescriptor(img, false); + Chart gchChart = cd.calculateGCH(img, false); + Chart histChart = cd.calculateHistogram(img, false); + Chart dcdChart = cd.calculateDCD(img, false); + Chart cldChart = cd.calculateCLD(img, false); + + File outputDir = new File(imgFile.getParent(), "output"); + if(!outputDir.exists()) { + outputDir.mkdirs(); + } + + BitmapEncoder.saveBitmapWithDPI(mcChart, outputDir.getCanonicalPath() + "/mc", BitmapFormat.PNG, 300); + BitmapEncoder.saveBitmapWithDPI(gchChart, outputDir.getCanonicalPath() + "/gch", BitmapFormat.PNG, 300); + BitmapEncoder.saveBitmapWithDPI(histChart, outputDir.getCanonicalPath() + "/hist", BitmapFormat.PNG, 300); + BitmapEncoder.saveBitmapWithDPI(dcdChart, outputDir.getCanonicalPath() + "/dcd", BitmapFormat.PNG, 300); + BitmapEncoder.saveBitmapWithDPI(cldChart, outputDir.getCanonicalPath() + "/cld", BitmapFormat.PNG, 300); + } + + + public Chart calculateMeanColorDescriptor(Mat img, boolean show) { + int rejectionSigma = 3; + MeanColorDescriptor meanColorDescriptor = new MeanColorDescriptor(rejectionSigma).calculate(img); + double[] meanColor = meanColorDescriptor.getMeanColorWithoutSigma(); + + // Create Chart + Color[] sliceColors = new Color[1]; + sliceColors[0] = new Color((int)meanColor[2], (int)meanColor[1], (int)meanColor[0]); + + + // Create Chart + PieChart chart = new PieChartBuilder().width(800).height(600).title("Mean color descriptor").build(); + + // Customize Chart + chart.getStyler().setSeriesColors(sliceColors); + + // Series + chart.addSeries(Integer.toString(1), 100); + + if(show) { + new SwingWrapper(chart).displayChart(); + } + + return chart; + } + + + public Chart calculateGCH(Mat img, boolean show) { + int dimension = 3; + GlobalColorHistogram gch = new GlobalColorHistogram(dimension).calculate(img); + double[] flattenGCH = gch.getFlattenGCH(); + + // Create Chart + CategoryChart chart = new CategoryChartBuilder().width(800).height(600).title("Global Color Histogram").xAxisTitle("Score").yAxisTitle("Number").build(); + + // Customize Chart + chart.getStyler().setLegendPosition(LegendPosition.InsideNW); + //chart.getStyler().setHasAnnotations(true); + + double[] labels = new double[dimension*dimension*dimension]; + + for(int i=0; i(chart).displayChart(); + } + + return chart; + } + + + public Chart calculateCLD(Mat img, boolean show) { + ColorLayoutDescriptor cld = new ColorLayoutDescriptor(12).calculate(img, null, null); + double[] descriptorB = cld.getBlueDescriptor(); + double[] descriptorG = cld.getGreenDescriptor(); + double[] descriptorR = cld.getRedDescriptor(); + + // Create Chart + CategoryChart chart = new CategoryChartBuilder().width(800).height(600).title("Color Layout Descriptor").xAxisTitle("Score").yAxisTitle("Number").build(); + + // Customize Chart + chart.getStyler().setLegendPosition(LegendPosition.InsideNW); + //chart.getStyler().setHasAnnotations(true); + + double[] labels = new double[] {1,2,3,4,5,6,7,8,9,10,11,12}; + + // Series + chart.addSeries("B", labels, descriptorB); + chart.addSeries("G", labels, descriptorG); + chart.addSeries("R", labels, descriptorR); + + if(show) { + new SwingWrapper(chart).displayChart(); + } + + return chart; + } + + + public Chart calculateDCD(Mat img, boolean show) { + int colorsAmount = 3; + + DominantColorDescriptor dcd = new DominantColorDescriptor(colorsAmount, 5, 5, DominantColorDescriptor.SORT_BY_BRIGHTNESS).calculate(img, null, null); + List dominantColors = dcd.getDominantColors(); + + System.out.println(dcd.getSparialCoherency()); + + Color[] sliceColors = new Color[colorsAmount]; + for(int i=0; i(chart).displayChart(); + } + + return chart; + } + + + public Chart calculateHistogram(Mat img, boolean show) { + SimpleHistograms hists = new SimpleHistograms(new Pair(0, 256), 64, SimpleHistograms.ImgType.COLOR).calculate(img, null, null); + double[] dataB = hists.getBlueHistogram(); + double[] dataG = hists.getGreenHistogram(); + double[] dataR = hists.getRedHistogram(); + + double[] labels = new double[dataB.length]; + for(int i=0; i(chart).displayChart(); + } + + return chart; + } + + + public static void printHelp() { + System.out.println("Usage: java -jar program.jar inputImage.png"); + } +} diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/GCH_ColorSpacies_Demo.java b/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/GCH_ColorSpacies_Demo.java new file mode 100644 index 0000000..701a218 --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/GCH_ColorSpacies_Demo.java @@ -0,0 +1,10 @@ +package ru.delkom07.improc.color; + +public class GCH_ColorSpacies_Demo { + + public static void main(String[] args) { + // TODO Auto-generated method stub + + } + +} diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/GCH_Demo.java b/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/GCH_Demo.java new file mode 100644 index 0000000..e510689 --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/GCH_Demo.java @@ -0,0 +1,83 @@ +package ru.delkom07.improc.color; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.Point; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; + +import ru.delkom07.improc.color.descriptors.GlobalColorHistogram; + +public class GCH_Demo { + private static final int rectHeight = 50; + private static final int rectWidth = 250; + private static final int gap = 20; + private static final int startX0 = 160; + private static final int startY0 = 30; + + + + public static void main(String[] args) throws IOException { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + + File outputDir = new File("../data/berners_seeds"); + + int dimension = 8; + GlobalColorHistogram gch = new GlobalColorHistogram(dimension); + + PrintWriter writer = new PrintWriter(new File(outputDir, "gch8.tsv"), "UTF-8"); + + int imgHeight = 2*startY0 + (gap+rectHeight)*dimension*dimension*dimension; + Mat gchImg = Mat.zeros(new Size(500, imgHeight), CvType.CV_8UC3); + + for(int i=0; i MAX_DESCRIPTOR_SIZE) { + descriptorSize = MAX_DESCRIPTOR_SIZE; + } + + this.descriptorSize = descriptorSize; + + descriptorR = new double[descriptorSize]; + descriptorG = new double[descriptorSize]; + descriptorB = new double[descriptorSize]; + } + + + public ColorLayoutDescriptor calculate(Mat img, Mat mask, Rect roi) { + if(null == roi) { + roi = new Rect(0, 0, img.cols(), img.rows()); + } + + int xBlockSize = roi.width/GRID_DIMENSION; + int yBlockSize = roi.height/GRID_DIMENSION; + + for(int xBlock=0; xBlock dominantColors = new ArrayList(); + private double spatialCoherency; + + private int descriptorSize; + + private int xStepSize; + private int yStepSize; + + private boolean calculated = false; + + public static final short SORT_BY_PERSENT = 0; + public static final short SORT_BY_BRIGHTNESS = 1; + + private short sortBy = 0; + + public DominantColorDescriptor(int descriptorSize) { + this.descriptorSize = descriptorSize; + + this.xStepSize = 1; + this.yStepSize = 1; + } + + + public DominantColorDescriptor(int descriptorSize, int xStepSize, int yStepSize, short sortBy) { + this.descriptorSize = descriptorSize; + + this.xStepSize = xStepSize > 0 ? xStepSize : 1; + this.yStepSize = yStepSize > 0 ? yStepSize : 1; + + this.sortBy = sortBy; + } + + + public DominantColorDescriptor calculate(Mat img, Mat mask, Rect roi) { + if(null == roi) { + roi = new Rect(0, 0, img.cols(), img.rows()); + } + + List> clusters = null != mask ? clusterizeColors(img, mask, roi, descriptorSize) : clusterizeColors(img, roi, descriptorSize); + + int clustersSumSize = 0; + for(CentroidCluster cluster : clusters) { + clustersSumSize += cluster.getPoints().size(); + } + + for(CentroidCluster cluster : clusters) { + List clusterColors = cluster.getPoints(); + + double[] clusterCenterColor = cluster.getCenter().getPoint(); + int clusterSize = cluster.getPoints().size(); + double variance = calculateColorVariance(clusterColors, clusterCenterColor); + + dominantColors.add(new DominantColor(clusterCenterColor, clusterSize/(double)clustersSumSize, variance)); + } + + switch(sortBy) { + case 1: { + dominantColors.sort(new Comparator() { + + @Override + public int compare(DominantColor o1, DominantColor o2) { + double val1 = o1.color[0]+o1.color[1]+o1.color[2]; + double val2 = o2.color[0]+o2.color[1]+o2.color[2]; + + if(val1val2) { + return 1; + } + return 0; + } + }); + break; + } + default: { + dominantColors.sort(new Comparator() { + + @Override + public int compare(DominantColor o1, DominantColor o2) { + if(o1.persento2.persent) { + return 1; + } + return 0; + } + }); + } + + } + + spatialCoherency = calculatespatialCoherency(img.size(), clusters, clustersSumSize); + + calculated = true; + + return this; + } + + + public List getDominantColors() { + if(!calculated) { + throw new RuntimeException("Descriptor was not calculated"); + } + + return dominantColors; + } + + + public double getSparialCoherency() { + if(!calculated) { + throw new RuntimeException("Descriptor was not calculated"); + } + + return spatialCoherency; + } + + + + private double calculateColorVariance(List colors, double[] clusterCenterColor) { + double sumSqrDeviance = 0; + for(ColorPixelContainer container : colors) { + sumSqrDeviance += GeometricFunctions.euclideanDistance(container.getPoint(), clusterCenterColor); + } + + return sumSqrDeviance/colors.size(); + } + + private double calculatespatialCoherency(Size size, List> clusters, int clustersSumSize) { + double coherency = 0; + + for(CentroidCluster cluster : clusters) { + List clusterColors = cluster.getPoints(); + + if(0 != clusterColors.size()) { + + double percentage = clusterColors.size() / clustersSumSize; + + Mat clusterImprint = Mat.zeros(size, CvType.CV_8UC1); + + for(ColorPixelContainer container : clusterColors) { + double[] point = container.getPoint(); + clusterImprint.put((int)point[1], (int)point[0], 255); + } + + int neighbors = 0; + for(int i=0; i= 0 ? (0 != clusterImprint.get(i-1, j)[0]) : false) { + neighbors++; + continue; + } + + if(j+1 < clusterImprint.cols() ? (0 != clusterImprint.get(i, j+1)[0]) : false) { + neighbors++; + continue; + } + + + if(i+1 < clusterImprint.rows() ? (0 != clusterImprint.get(i+1, j)[0]) : false) { + neighbors++; + continue; + } + + if(j-1 >= 0 ? (0 != clusterImprint.get(i, j-1)[0]) : false) { + neighbors++; + continue; + } + } + } + } + + coherency += (neighbors / (double) clusterColors.size()) * percentage; + + clusterImprint.release(); + } + } + + return coherency; + } + + + + + private List> clusterizeColors(Mat img, Mat mask, Rect roi, int clusterNumber) { + KMeansPlusPlusClusterer kmeansClusterer = new KMeansPlusPlusClusterer(clusterNumber); + MultiKMeansPlusPlusClusterer mkmeansClusterer = new MultiKMeansPlusPlusClusterer<>(kmeansClusterer, 1); + + List points = new LinkedList(); + + for(int i=roi.y; i> clusterizeColors(Mat img, Rect roi, int clusterNumber) { + KMeansPlusPlusClusterer kmeansClusterer = new KMeansPlusPlusClusterer(clusterNumber); + MultiKMeansPlusPlusClusterer mkmeansClusterer = new MultiKMeansPlusPlusClusterer<>(kmeansClusterer, 1); + + List points = new LinkedList(); + + for(int i=roi.y; i= 8) { + System.out.println("FFF"); + } + + if(mask.get(i, j)[0] != 0) { + gch[(int)(pix[0]/ch1BinSize)][(int)(pix[1]/ch2BinSize)][(int)(pix[2]/ch3BinSize)]++; + pixelsAmount++; + } + } + } + + for(int i=0; ifrom[0] && pix[0]from[1] && pix[1]from[2] && pix[2] from && pix[channel] < to) { + img.put(i, j, color); + } + } + } + } + } +} diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/descriptors/SimpleHistograms.java b/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/descriptors/SimpleHistograms.java new file mode 100644 index 0000000..65806ba --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/improc/color/descriptors/SimpleHistograms.java @@ -0,0 +1,177 @@ +package ru.delkom07.improc.color.descriptors; + +import java.util.Arrays; + +import org.opencv.core.Mat; +import org.opencv.core.MatOfFloat; +import org.opencv.core.MatOfInt; +import org.opencv.core.Rect; +import org.opencv.imgproc.Imgproc; + +import ru.delkom07.util.Pair; + +public class SimpleHistograms { + private Pair range; + private int histogramSize; + + private Mat histGray = new Mat(); + + private Mat histB = new Mat(); + private Mat histG = new Mat(); + private Mat histR = new Mat(); + + private ImgType imgType; + + private boolean calculated = false; + + public enum ImgType { + GRAY, COLOR + } + + + + public SimpleHistograms(Pair range, int histSize, ImgType imgType) { + this.range = range; + this.histogramSize = histSize; + + this.imgType = imgType; + } + + + public SimpleHistograms calculate(Mat img, Mat mask, Rect roi) { + if(null == mask) { + mask = new Mat(); + } + + if(null != roi) { + img = img.submat(roi); + mask = mask.submat(roi); + } + + MatOfFloat ranges = new MatOfFloat(range.getLeft(), range.getRight()); + MatOfInt histSize = new MatOfInt(histogramSize); + + if(imgType == ImgType.COLOR) { + Imgproc.calcHist(Arrays.asList(img), new MatOfInt(0), mask, histB, histSize, ranges); + Imgproc.calcHist(Arrays.asList(img), new MatOfInt(1), mask, histG, histSize, ranges); + Imgproc.calcHist(Arrays.asList(img), new MatOfInt(2), mask, histR, histSize, ranges); + } else { + Imgproc.calcHist(Arrays.asList(img), new MatOfInt(), new Mat(), histGray, histSize, ranges); + } + + calculated = true; + + return this; + } + + public Mat getGrayHistogramMat() { + if(imgType != ImgType.GRAY || !calculated) { + throw new RuntimeException("Gray histogram mat isn't calculated"); + } + + return histGray; + } + + public Mat getBlueColorHistogramMat() { + if(imgType != ImgType.COLOR || !calculated) { + throw new RuntimeException("Color histogram mats isn't calculated"); + } + + return histB; + } + + public Mat getGreenColorHistogramMat() { + if(imgType != ImgType.COLOR || !calculated) { + throw new RuntimeException("Color histogram mats isn't calculated"); + } + + return histG; + } + + public Mat getRedColorHistogramMat() { + if(imgType != ImgType.COLOR || !calculated) { + throw new RuntimeException("Color histogram mats isn't calculated"); + } + + return histR; + } + + + + + + public double[] getGrayHistogram() { + if(imgType != ImgType.GRAY || !calculated) { + throw new RuntimeException("Gray histogram mat isn't calculated"); + } + + double[] dataGray = new double[histGray.cols()*histGray.rows()]; + + int k = 0; + for(int i=0; i maxDistance) { + maxP1 = p1; + maxP2 = p2; + maxDistance = GeometricFunctions.distance(p1, p2); + } + } + + } + + return new Point[]{maxP1, maxP2}; + } + + + + /** + * Вернить две точки лежащие наиболее близко к данной линии и нормали справа и слева от нормали соответственно. + * Функция сначала находит список точек претендентов: наиболее близкие к линии точки по pointBufferSize справа и слева. + * После этого для каждой стороны выбирается наиболее близкая точка к нормали. + * @param listOfPoints - списко точек. + * @param line - данная линия. + * @param normal - нормаль. + * @return две точки лежащие наиболее близко к данной линии и нормали справа и слева от нормали соответственно. + */ + /* + public static Point[] getTwoPointThatLayOnGivenLine(List listOfPoints, Vector line, Vector normal) { + short pointBufferSize = 5; + + // наборы правых и левых точек контура относительно normal, лежащие близко к line + List> leftPoints = new LinkedList>(); + List> rightPoints = new LinkedList>(); + + for(Point point : listOfPoints) { + double xDiff = (line.x2() - line.x1()); + double yDiff = (line.y2() - line.y1()); + + xDiff = xDiff == 0 ? 0.0001 : xDiff; // STUB + yDiff = yDiff == 0 ? 0.0001 : yDiff; // STUB + + // расстояние от точки до прямой line + double closenessToLine = Math.abs((point.x - line.x1())/xDiff - (point.y - line.y1())/yDiff); + //double closenessToLine = Math.abs((point.x - line.x1())/(line.x2() - line.x1()) - (point.y - line.y1())/(line.y2()-line.y1())); + + // position relative to the normal + // (y1-y2)*x+(x2-x1)*y+(x1*y2-x2*y1) > or < 0 + double position = (normal.y1()-normal.y2())*point.x + (normal.x2()-normal.x1()) * point.y + (normal.x1()*normal.y2()-normal.x2()*normal.y1()); + + // компаратор для пары Точка-Значение. сравнивает пары по значению. + Comparator> comparator = new Comparator>() { + + @Override + public int compare(Pair arg0, Pair arg1) { + if(arg0.getValue() > arg1.getValue()) { + return 1; + } + if(arg0.getValue() < arg1.getValue()) { + return -1; + } + return 0; + } + }; + + if(position <= 0) { // right position + if(rightPoints.size() < pointBufferSize && !Double.isNaN(closenessToLine) && Double.isFinite(closenessToLine)) { + rightPoints.add(new Pair(point, closenessToLine)); + + if(rightPoints.size() == pointBufferSize) { + rightPoints.sort(comparator); + } + + } else { + for(int j=0; j rightPoint = rightPoints.get(j); + + if(closenessToLine < rightPoint.getValue()) { + rightPoints.add(rightPoints.indexOf(rightPoint), new Pair(point, closenessToLine)); + rightPoints.remove(rightPoints.size()-1); + break; + } + } + + } + + } else { // left position + if(leftPoints.size() < pointBufferSize && !Double.isNaN(closenessToLine) && Double.isFinite(closenessToLine)) { + leftPoints.add(new Pair(point, closenessToLine)); + + if(leftPoints.size() == pointBufferSize) { + leftPoints.sort(comparator); + } + } else { + for(int j=0; j leftPoint = leftPoints.get(j); + + if(closenessToLine < leftPoint.getValue()) { + leftPoints.add(leftPoints.indexOf(leftPoint), new Pair(point, closenessToLine)); + leftPoints.remove(leftPoints.size()-1); + break; + } + } + + } + } + } + + + Point rightPoint = null; + double rightBestDist = Double.MAX_VALUE; + for(Pair rightPointPair : rightPoints) { + Point point = rightPointPair.getKey(); + + double xDiff = (normal.x2() - normal.x1()); + double yDiff = (normal.y2() - normal.y1()); + + xDiff = xDiff == 0 ? 0.0001 : xDiff; + yDiff = yDiff == 0 ? 0.0001 : yDiff; + + double closenessToNormal = Math.abs((point.x - normal.x1())/xDiff - (point.y - normal.y1())/yDiff); + //double closenessToNormal = Math.abs((point.x - normal.x1())/(normal.x2() - normal.x1()) - (point.y - normal.y1())/(normal.y2()-normal.y1())); + + if(closenessToNormal < rightBestDist) { + rightPoint = point; + rightBestDist = closenessToNormal; + } + + } + + Point leftPoint = null; + double leftBestDist = Double.MAX_VALUE; + for(Pair leftPointPair : leftPoints) { + Point point = leftPointPair.getKey(); + + double xDiff = (normal.x2() - normal.x1()); + double yDiff = (normal.y2()- normal.y1()); + + xDiff = xDiff == 0 ? 0.0001 : xDiff; + yDiff = yDiff == 0 ? 0.0001 : yDiff; + + double closenessToNormal = Math.abs((point.x - normal.x1())/xDiff - (point.y - normal.y1())/yDiff); + //double closenessToNormal = Math.abs((point.x - normal.x1())/(normal.x2() - normal.x1()) - (point.y - normal.y1())/(normal.y2()-normal.y1())); + + if(closenessToNormal < leftBestDist) { + leftPoint = point; + leftBestDist = closenessToNormal; + } + } + + return new Point[]{leftPoint, rightPoint}; + } + */ + + + public static Point[] getTwoPointThatLayOnGivenLine(List listOfPoints, Vector line, Vector normal) { + // правые и левые точки контура относительно normal, лежащие близко к line + Pair rightPoint = null; + Pair leftPoint = null; + + for(Point point : listOfPoints) { + double xDiff = (line.x2() - line.x1()); + double yDiff = (line.y2() - line.y1()); + + //xDiff = xDiff == 0 ? 0.0001 : xDiff; // STUB + //yDiff = yDiff == 0 ? 0.0001 : yDiff; // STUB + + // расстояние от точки до прямой line + //double closenessToLine = Math.abs((point.x - line.x1())/xDiff - (point.y - line.y1())/yDiff); + + double closenessToLineX = xDiff != 0 ? (point.x - line.x1())/xDiff : 0; + double closenessToLineY = yDiff != 0 ? (point.y - line.y1())/yDiff : 0; + double closenessToLine = Math.abs(closenessToLineX - closenessToLineY); + + //double closenessToLine = Math.abs((point.x - line.x1())/(line.x2() - line.x1()) - (point.y - line.y1())/(line.y2()-line.y1())); + + // position relative to the normal + // (y1-y2)*x+(x2-x1)*y+(x1*y2-x2*y1) > or < 0 + double position = (normal.y1()-normal.y2())*point.x + (normal.x2()-normal.x1()) * point.y + (normal.x1()*normal.y2()-normal.x2()*normal.y1()); + + if(Double.isNaN(closenessToLine) || !Double.isFinite(closenessToLine)) { + continue; + } else { + if(position <= 0) { // right position + if(null == rightPoint) { + rightPoint = new Pair(point, closenessToLine); + } else { + + if(closenessToLine < rightPoint.getRight()) { + rightPoint = new Pair(point, closenessToLine); + } + + + } + } else { + if(null == leftPoint) { + leftPoint = new Pair(point, closenessToLine); + } else { + + if(closenessToLine < leftPoint.getRight()) { + leftPoint = new Pair(point, closenessToLine); + } + + } + } + } + + } + + return new Point[]{leftPoint != null ? leftPoint.getLeft() : null, rightPoint != null ? rightPoint.getLeft() : null}; + } + + + + public static Vector normalize(Vector vector) { + double length = length(vector); + + double newX = vector.x()/length; + double newY = vector.y()/length; + + return new Vector(vector.x1(), vector.y1(), vector.x1()+newX, vector.y1()+newY); + } + + + /** + * Calculate of angle between given lines in degrees. + * To calculate radians: *pi/180. + * @param vect1 - first vector. + * @param vect2 - second vector. + * @return angle between given lines. + */ + public static double angle(Vector vect1, Vector vect2) { + return Math.acos((vect1.x()*vect2.x()+vect1.y()*vect2.y())/(vect1.length()*vect2.length())) * 180 / Math.PI; + } + + + /** + * Angle between two lines (without mean directions). + * @param vect1 - first line. + * @param vect2 - second line. + * @return Angle between two lines. + */ + public static double absLineAngle(Vector vect1, Vector vect2) { + double val = Math.abs(Math.acos((vect1.x()*vect2.x()+vect1.y()*vect2.y())/(vect1.length()*vect2.length())) * 180 / Math.PI); + + return val < 90 ? val : 180 - val; + } + + + /** + * Calculate of angle between given lines in degrees. + * To calculate radians: *pi/180. + * @param line1 - first line. + * @param line2 - second line. + * @return angle between given lines. + */ + public static double angle(double[] line1, double[] line2) { + double vect1X = Math.abs(line1[0]-line1[2]); // |x1 - x2| + double vect1Y = Math.abs(line1[1]-line1[3]); // |y1 - y2| + double length1 = vectLenght(vect1X, vect1Y); + + double vect2X = Math.abs(line2[0]-line2[2]); // |x1 - x2| + double vect2Y = Math.abs(line2[1]-line2[3]); // |y1 - y2| + double length2 = vectLenght(vect2X, vect2Y); + + return Math.acos((vect1X*vect2X+vect1Y*vect2Y)/(length1*length2)) * 180 / Math.PI; + } + + + /** + * Calculate of angle between given vector in degrees. + * To calculate radians: *pi/180. + * @param vect1 - first vector. + * @param vect2 - second vector. + * @return angle between given lines. + */ + public static double vectorAngle(double[] vect1, double[] vect2) { + double vect1X = vect1[2]-vect1[0]; // x2 - x1 + double vect1Y = vect1[3]-vect1[1]; // y2 - y1 + double length1 = vectLenght(vect1X, vect1Y); + + double vect2X = vect2[2]-vect2[0]; // x2 - x1 + double vect2Y = vect2[3]-vect2[1]; // y2 - y1 + double length2 = vectLenght(vect2X, vect2Y); + + return Math.acos((vect1X*vect2X+vect1Y*vect2Y)/(length1*length2)) * 180 / Math.PI; + } + + + public static double vectLenght(double x, double y) { + return Math.sqrt(x*x+y*y); + } + + public static Point[] getTopLeftAndBottomRight(List points) { + double minX = Integer.MAX_VALUE; + double minY = Integer.MAX_VALUE; + + double maxX = -1; + double maxY = -1; + for(Point point : points) { + if(point.x < minX) { + minX = point.x; + } + + if(point.y < minY) { + minY = point.y; + } + + if(point.x > maxX) { + maxX = point.x; + } + + if(point.y > maxY) { + maxY = point.y; + } + } + + return new Point[] {new Point(minX, minY), new Point(maxX, maxY)}; + } + + + +} + + +/* + * + * public static Point[] getTwoPointThatLayOnGivenLine_(List listOfPoints, Vector line) { + Point point1 = null; + Point point2 = null; + Point prevPoint = listOfPoints.get(0); + + boolean rele = false; + + double minEstimation = Double.MAX_VALUE; + for(Point point : listOfPoints) { + double estimation = Math.abs((point.x - line.x1())/(line.x2() - line.x1()) - (point.y - line.y1())/(line.y2()-line.y1())); + + if(estimation < minEstimation && distance(prevPoint, point)>10) { // ?????? distance it is not good decision + minEstimation= estimation; + + if(rele) { + point1 = point; + } else { + point2 = point; + } + rele = !rele; + prevPoint = point; + } + + } + + return new Point[]{point1, point2}; + } + */ diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/improc/geometry/Vector.java b/mask-color-descriptors/src/main/java/ru/delkom07/improc/geometry/Vector.java new file mode 100644 index 0000000..cc9f091 --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/improc/geometry/Vector.java @@ -0,0 +1,103 @@ +package ru.delkom07.improc.geometry; + +import org.opencv.core.Mat; +import org.opencv.core.Point; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; + +public class Vector { + private Double x; + private Double y; + + private Double x1; + private Double y1; + private Double x2; + private Double y2; + + boolean coordinateAssigned = false; + + + + public Vector(double x, double y) { + this.x = x; + this.y = y; + + coordinateAssigned = false; + } + + public Vector(double x1, double y1, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + + this.x = x2-x1; + this.y = y2-y1; + + coordinateAssigned = true; + } + + public Double x() { + return x; + } + + public Double y() { + return y; + } + + public Double x1() { + if(!coordinateAssigned) + throw new RuntimeException("Coordinate not assigned"); + + return x1; + } + + public Double y1() { + if(!coordinateAssigned) + throw new RuntimeException("Coordinate not assigned"); + + return y1; + } + + public Double x2() { + if(!coordinateAssigned) + throw new RuntimeException("Coordinate not assigned"); + + return x2; + } + + public Double y2() { + if(!coordinateAssigned) + throw new RuntimeException("Coordinate not assigned"); + + return y2; + } + + public void setPoint1(Point p1) { + coordinateAssigned = true; + this.x1 = p1.x; + this.y1 = p1.y; + + this.x = x2-x1; + this.y = y2-y1; + } + + public void setPoint2(Point p2) { + coordinateAssigned = true; + + this.x2 = p2.x; + this.y2 = p2.y; + + this.x = x2-x1; + this.y = y2-y1; + } + + public Double length() { + return Math.sqrt(x*x+y*y); + } + + public void draw(Mat img, Scalar color) { + Imgproc.line(img, new Point(x1(), y1()), new Point(x2(), y2()), color, 1); + //Imgproc.circle(img, new Point(x2(), y2()), 10, color, -1); + } +} diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/GLCM.java b/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/GLCM.java new file mode 100644 index 0000000..eef2176 --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/GLCM.java @@ -0,0 +1,369 @@ +package ru.delkom07.improc.texture; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.opencv.core.Mat; + +/** + * Realization of Gray level co-occurrence matrix. + * + * В текущей версии вычисляется матрица для всех основных принципиальных направлений (в определении матрицы GLCM это параметр, как и d). + * + * @author Komyshev + */ +public class GLCM { + int Nx; // ширина изображения (параметр матрицы GLCM) + int Ny; // высота изображения (параметр матрицы GLCM) + + int Ng = 256; // квантификация уровней серого (параметр матрицы GLCM) + int d = 1; // расстояние (параметр матрицы GLCM) + + int[][] P; // ненормализованная матрица GLCM + double[][] p; // нормализованная матрица GLCM + + boolean calculated = false; + + // вычисление + // все вычисляемые направления (параметр матрицы GLCM) + static final int directions[][] = { + {0, 1}, {0, -1}, // by vertical + {1, 0}, {-1, 0}, // by horizontal + + {1, 1}, {-1, -1}, // 1 diagonal + {1, -1}, {-1, 1}, // 2 diagonal + }; + + + public GLCM() {} + + public GLCM(int d) { + this.d = d; + } + + public GLCM(int Ng, int d) { + this.Ng = Ng; + this.d = d; + } + + + public GLCM calculate(Mat gray) { + P = new int[Ng][Ng]; + p = new double[Ng][Ng]; + + Nx = gray.cols(); + Ny = gray.rows(); + + initP(gray); + + // (3*) нормализация + int C = 2*Nx*(Ny - 1) + 2*Ny*(Nx - 1) + 4*(Nx - 1)*(Ny - 1); + normalizeP(C); + + calculated = true; + + return this; + } + + /** + * (1*) Инициализировать матрицу P (ненормализованную) - посчитать число соотвутствующих пар пикселей. + * @param gray + */ + private void initP(Mat gray) { + for(int x=0; x= 0 && nextX < Nx && nextY >= 0 && nextY < Ny) { + int jPix = (int) gray.get(nextY, nextX)[0]; + + P[iPix][jPix]++; + } + } + } + } + } + + /* + * (2*) Вычислить нормализованную GLCM матрицу. + */ + void normalizeP(int C) { + for(int i=0; i max) { + max = p[i-1][j-1]; + } + } + } + + return max; + } + + /** + * (9*) Correlation. + * @return + */ + public double correlation() { + initCheck(); + + double res = 0; + + double mu = mean(); + double sigma2 = variance(); + + for(int i=1; i<=Ng; i++) { + for(int j=1; j<=Ng; j++) { + res += ( ( (i-mu)*(j-mu) )/sigma2 ) * p[i-1][j-1]; + } + } + + return res; + } + + + /** + * (10*) Homogeneity. + * @return + */ + public double homogeneity() { + initCheck(); + + double res = 0; + + for(int i=1; i<=Ng; i++) { + for(int j=1; j<=Ng; j++) { + res += ( 1 / (1 + Math.pow(i-j, 2) ) ) * p[i-1][j-1]; + } + } + + return res; + } + + /** + * (11*) Inertia. + * @return + */ + public double inertia() { + initCheck(); + + double res = 0; + + for(int i=1; i<=Ng; i++) { + for(int j=1; j<=Ng; j++) { + res += Math.pow(i-j, 2) * p[i-1][j-1]; + } + } + + return res; + } + + /** + * (12*) Cluster shade. + * @return + */ + public double clusterShade() { + initCheck(); + + double res = 0; + + double mu = mean(); + + for(int i=1; i<=Ng; i++) { + for(int j=1; j<=Ng; j++) { + res += Math.pow(i + j - 2*mu, 3) * p[i-1][j-1]; + } + } + + return res; + } + + /** + * (13*) Cluster prominance. + * @return + */ + public double clusterProminance() { + initCheck(); + + double res = 0; + + double mu = mean(); + + for(int i=1; i<=Ng; i++) { + for(int j=1; j<=Ng; j++) { + res += Math.pow(i + j - 2*mu, 4) * p[i-1][j-1]; + } + } + + return res; + } + + + + // == Вывод == + public static String[] ChNames() { + return new String[] { + "mean", "variance", + "uniformity", "entropy", "maxProbability", "correlation", + "homogeneity", "inertia", "clusterShade", "clusterProminance" + }; + } + + + /** + * Вывести дескриптор как массив характеристик. + * @return массив характеристик дескриптора. + */ + public double[] toArray() { + return new double[] { + mean(), variance(), + uniformity(), entropy(), maxProbability(), correlation(), + homogeneity(), inertia(), clusterShade(), clusterProminance() + }; + } + + + /** + * Вывести дескриптор как строку. + * @return строковое представление дескриптора. + */ + public String toTsvCsvString(String sep) { + List list = Arrays.stream(toArray()).boxed().collect(Collectors.toList()); + + String joined = list.stream() + .map(d -> d.toString()) + .collect(Collectors.joining(sep)); + + return joined; + } + + +} diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/GLRM.java b/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/GLRM.java new file mode 100644 index 0000000..d3c25d2 --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/GLRM.java @@ -0,0 +1,419 @@ +package ru.delkom07.improc.texture; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.opencv.core.Mat; + +/** + * Realization of Gray level run length matrix. + * + * @author Komyshev + */ +public class GLRM { + int Ng = 256; // квантификация уровней серого + int Nr = 4; // максимальная длина серий уровней серого, определяемая в данной матрице + + int[][] q; // матрица GLRM + int R; // сумма матрицы GLRM + + boolean calculated = false; // флаг инициализации дескриптора для проверки перед запросом характеристик или матриц + + + /** + * Основной конструктор. + * @param maxLength - максимальная учитываемая длина серии. + */ + public GLRM(int maxLength) { + this.Nr = maxLength; + } + + + /** + * Конструктор с указанием квантификации уровня серого. + * @param Ng - квантификация уровней серого. + * @param maxLength - максимальная учитываемая длина серии. + */ + public GLRM(int Ng, int maxLength) { + this.Ng = Ng; + this.Nr = maxLength; + } + + + public GLRM calculate(Mat gray) { + q = new int[Ng][Nr]; + + countRunsOn_0(gray); + countRunsOn_90(gray); + countRunsOn_135(gray); + countRunsOn_45(gray); + + R = calculateR(); + + calculated = true; + + return this; + } + + + /** + * Посчитать сериии по горизонтали (слева на право). + * @param gray - изображение в градациях серого. + */ + private void countRunsOn_0(Mat gray) { + for(int y=0; y= gray.cols()) { + count(curLvl, curLen); + break; + } + + int nextPoint = (int) gray.get(y, x)[0]; + + if(curLvl != nextPoint) { + count(curLvl, curLen); + + // flash + curLvl = nextPoint; + curLen = 1; + + } else { + curLen++; + } + + } // end for cols + + } // end for rows + } + + + /** + * Посчитать серии по вертикали (сверху вниз). + * @param gray - изображение в градациях серого. + */ + private void countRunsOn_90(Mat gray) { + for(int x=0; x=gray.rows()) { + count(curLvl, curLen); + break; + } + + int nextPoint = (int) gray.get(y, x)[0]; + + if(curLvl != nextPoint) { + count(curLvl, curLen); + + // flash + curLvl = nextPoint; + curLen = 1; + } else { + curLen++; + } + + } // end for rows + + } // end for cols + } + + + /** + * По диагонали слева-сверху -> вправо-вниз + * @param gray + */ + private void countRunsOn_135(Mat gray) { + for(int c=0; c вправо-вверх + * @param gray + */ + private void countRunsOn_45(Mat gray) { + for(int r=0; r= gray.cols() || nextY < 0 || nextY >= gray.rows()) { + count(curLvl, curLen); + break; + } + + int nextLvl = (int) gray.get(nextY, nextX)[0]; + + if(curLvl != nextLvl) { // уровень серого изменился + count(curLvl, curLen); + + // flash + curLvl = nextLvl; + curLen = 1; + + curX = nextX; + curY = nextY; + } else { // новый уровень серого + curLen++; + } + } + } + + + void count(int lvl, int len) { + int lenPos = len <= Nr ? len-1 : Nr-1; + q[lvl][lenPos]++; + } + + + /** + * Вычислить делитель нормализации. + * По сути просто суммирует матрицу q. + * (20*) + * @return делитель нормализации. + */ + int calculateR() { + + int res = 0; + + for(int i=0; i list = Arrays.stream(toArray()).boxed().collect(Collectors.toList()); + + String joined = list.stream() + .map(d -> d.toString()) + .collect(Collectors.joining(sep)); + + return joined; + } + +} diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/MaskedGLCM.java b/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/MaskedGLCM.java new file mode 100644 index 0000000..9813c54 --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/MaskedGLCM.java @@ -0,0 +1,83 @@ +package ru.delkom07.improc.texture; + +import org.opencv.core.Mat; + +/** + * Realization of Gray level co-occurrence matrix with masks. + * При вычислении учитываются только пиксели соотвествующие не нулевым пикселя маски. + * При встрече нулевого пикселя маски рассматривается такое же поведение, как выход за границы изображения. + * + * В текущей версии вычисляется матрица для всех основных принципиальных направлений (в определении матрицы GLCM это параметр, как и d). + * + * @author Komyshev + */ +public class MaskedGLCM extends GLCM { + Mat mask; + + public MaskedGLCM() { + super(); + } + + + public MaskedGLCM(int d) { + super(d); + } + + + public MaskedGLCM(int Ng, int d) { + super(Ng, d); + } + + + public MaskedGLCM calculate(Mat gray, Mat mask) { + P = new int[Ng][Ng]; + p = new double[Ng][Ng]; + + Nx = gray.cols(); + Ny = gray.rows(); + + initP(gray, mask); + + // (3*) нормализация + int C = 2*Nx*(Ny - 1) + 2*Ny*(Nx - 1) + 4*(Nx - 1)*(Ny - 1); + normalizeP(C); + + calculated = true; + + return this; + } + + /** + * (1*) Инициализировать матрицу P (ненормализованную) - посчитать число соотвутствующих пар пикселей. + * @param gray + */ + void initP(Mat gray, Mat mask) { + for(int x=0; x= 0 && nextX < Nx && nextY >= 0 && nextY < Ny) { + + if(0 != mask.get(nextY, nextX)[0]) { // only mask pixels + int jPix = (int) gray.get(nextY, nextX)[0]; + + P[iPix][jPix]++; + } + + } + } + } + + } + } + } + + +} diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/MaskedGLRM.java b/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/MaskedGLRM.java new file mode 100644 index 0000000..38d571b --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/improc/texture/MaskedGLRM.java @@ -0,0 +1,207 @@ +package ru.delkom07.improc.texture; + +import org.opencv.core.Mat; + +/** + * Realization of Gray level run length matrix with masks. + * При вычислении учитываются только пиксели соотвествующие не нулевым пикселя маски. + * При встрече нулевого пикселя маски рассматривается такое же поведение, как выход за границы изображения. + * @author Komyshev + */ +public class MaskedGLRM extends GLRM { + /** + * Основной конструктор. + * @param maxLength - максимальная учитываемая длина серии. + */ + public MaskedGLRM(int maxLength) { + super(maxLength); + } + + + /** + * Конструктор с указанием квантификации уровня серого. + * @param Ng - квантификация уровней серого. + * @param maxLength - максимальная учитываемая длина серии. + */ + public MaskedGLRM(int Ng, int maxLength) { + super(Ng, maxLength); + } + + + public MaskedGLRM calculate(Mat gray, Mat mask) { + q = new int[Ng][Nr]; + + countRunsOn_0(gray, mask); + countRunsOn_90(gray, mask); + countRunsOn_135(gray, mask); + countRunsOn_45(gray, mask); + + R = calculateR(); + + calculated = true; + + return this; + } + + + /** + * Посчитать сериии по горизонтали (слева на право). + * @param gray - изображение в градациях серого. + */ + private void countRunsOn_0(Mat gray, Mat mask) { + for(int y=0; y= gray.cols()) { + count(curLvl, curLen); + break; + } + + int nextPoint = (int) gray.get(y, x)[0]; + int nextMaskPoint = (int) mask.get(y, x)[0]; // only mask pixels + + if(0 == nextMaskPoint || curLvl != nextPoint) { // only mask pixels + count(curLvl, curLen); + + // flash + curLvl = nextPoint; + curLen = 1; + + } else { + curLen++; + } + + } // end for cols + + } // end for rows + } + + + /** + * Посчитать серии по вертикали (сверху вниз). + * @param gray - изображение в градациях серого. + */ + private void countRunsOn_90(Mat gray, Mat mask) { + for(int x=0; x=gray.rows()) { + count(curLvl, curLen); + break; + } + + int nextPoint = (int) gray.get(y, x)[0]; + int nextMaskPoint = (int) mask.get(y, x)[0]; // only mask pixels + + if(0 == nextMaskPoint || curLvl != nextPoint) { // only mask pixels + count(curLvl, curLen); + + // flash + curLvl = nextPoint; + curLen = 1; + } else { + curLen++; + } + + } // end for rows + + } // end for cols + } + + + /** + * По диагонали слева-сверху -> вправо-вниз + * @param gray + */ + private void countRunsOn_135(Mat gray, Mat mask) { + for(int c=0; c вправо-вверх + * @param gray + */ + private void countRunsOn_45(Mat gray, Mat mask) { + for(int r=0; r= gray.cols() || nextY < 0 || nextY >= gray.rows()) { + count(curLvl, curLen); + break; + } + + int nextLvl = (int) gray.get(nextY, nextX)[0]; + int nextMaskLvl = (int) mask.get(nextY, nextX)[0]; + + if(0 == nextMaskLvl || curLvl != nextLvl) { // уровень серого изменился + count(curLvl, curLen); + + // flash + curLvl = nextLvl; + curLen = 1; + + curX = nextX; + curY = nextY; + } else { // новый уровень серого + curLen++; + } + } + } + +} diff --git a/mask-color-descriptors/src/main/java/ru/delkom07/pendent/DiscreteCosineTransform_direct_wrong_realization.java b/mask-color-descriptors/src/main/java/ru/delkom07/pendent/DiscreteCosineTransform_direct_wrong_realization.java new file mode 100644 index 0000000..88f7a47 --- /dev/null +++ b/mask-color-descriptors/src/main/java/ru/delkom07/pendent/DiscreteCosineTransform_direct_wrong_realization.java @@ -0,0 +1,83 @@ +package ru.delkom07.pendent; + +import org.opencv.core.Mat; +import org.opencv.core.Rect; + +public class DiscreteCosineTransform_direct_wrong_realization { + private Mat transformed; + private static final double oneDivSqrt2 = 1.0/Math.sqrt(2.0); + + public void transform(Mat img, Mat transformed, boolean inv) { + this.transformed = transformed; + + int blockSize = 8; + int xBlocks = img.cols()/blockSize; + int yBlocks = img.rows()/blockSize; + + for(int xBlock=0; xBlock inputAndOutputs = workspace.getRawOpUnits(); + + File ccDataFile = new File(outputDir, "colorcheckersign.csv"); + File wsDataFile = new File(outputDir, "whitesquaresign.csv"); + + + try ( + PrintWriter ccWriter = new PrintWriter(ccDataFile); + PrintWriter wsWriter = new PrintWriter(wsDataFile); + ) { + ccWriter.print("File name" + SEPARATOR); + wsWriter.println("File name" + SEPARATOR); + + printHeader(ccWriter, SEPARATOR); + + System.out.println("Total imgs amount: " + inputAndOutputs.size() + "\n"); + + int count = 1; + for(OpUnit inputAndOutput : inputAndOutputs) { + File inputFile = inputAndOutput.getIn(); + File outputFile = inputAndOutput.getOut(); + + Mat inputImg = null; + try { + inputImg = Imgcodecs.imread(inputFile.getCanonicalPath()); + + System.out.println(count++ + " " + inputFile.getCanonicalPath() + "... "); + + MyColorChecker myColorChecker = new MyColorChecker(inputImg); + + ColorCheckerSign sign = myColorChecker.getSignFrom(inputImg); + sign.printColorDescriptors(outputFile, inputFile.getName(), ccWriter, SEPARATOR); + + Quad ccQuad = myColorChecker.getColorChecherQuad(); + + Rect whiteSquareRect = new Rect((int)ccQuad.tr().x+100, (int)ccQuad.tr().y-100, 300, 300); + printAddWhiteBackgroundSquare(inputFile.getName(), inputImg, whiteSquareRect, wsWriter, SEPARATOR); + + Mat extractedCCImg = sign.getColorCheckerImg(); + + sign.drawRects(extractedCCImg); + Mat whiteSquare = inputImg.submat(whiteSquareRect); + + + Mat submat = extractedCCImg.submat(new Rect(0, 0, whiteSquareRect.width, whiteSquareRect.height)); + whiteSquare.copyTo(submat); + + Imgcodecs.imwrite(outputFile.getCanonicalPath(), inputImg); + Imgcodecs.imwrite(outputFile.getCanonicalPath().replace(".jpg", "_cc.png"), extractedCCImg); + } catch(Exception e) { + e.printStackTrace(); + System.err.println("error. File was skipped."); + } finally { + inputImg.release(); + } + + ccWriter.flush(); + wsWriter.flush(); + } + + } catch (FileNotFoundException fnfe) { + fnfe.printStackTrace(); + } + + + } + + + private static void printAddWhiteBackgroundSquare(String fileName, Mat img, Rect rect, PrintWriter writer, String separator) { + ColorCheckerSample addWhiteSquareSample = new ColorCheckerSample(img, rect); + + for(int i=0; i img.cols() || roi.y + roi.height > img.rows()) { + System.out.println(); + } + + + sampleParts[num] = img.submat(samplePartsRects[num]); + } + } + } + + Rect[] getRects() { + return samplePartsRects; + } + + Mat[] getSampleParts() { + return sampleParts; + } +} + + + + + + + + + + + + + + + + + + + + + + +/* +//colors[(i*SPLIT) + j] = getMiddleColor(img, new Point(tl.x + xStep*j, tl.y + yStep*i), +// new Point(tl.x + xStep*(j+1), tl.y + yStep*(i+1))); + +private double[] getMiddleColor(Mat img, Point tl, Point br) { +double[] middle = new double[] {0, 0, 0}; + +int count = 0; +for(int x = (int)tl.x; x calibrateImg() { + return colorChecker.calibrate(img, model, ColorSpace.RGB, ColorSpace.RGB); + } + + public double getPixelArea() { + return colorChecker.pixelArea(); + } + + public double getPixPerMM() { + return 1/Math.sqrt(colorChecker.pixelArea()); + } + + public ColorCheckerSign getSignFrom(Mat img) { + Mat extractedColorChecker = getTransformedField(img, ccQuad); + + return new ColorCheckerSign(extractedColorChecker); + } + + + public Mat getTransformedField(Mat image, Quad ccQuad) { + // Define the destination image + Mat transformed = new Mat(2791, 1645, image.type()); + + // Corners of the destination image + Point[] quad_pts = new Point[4]; + quad_pts[0] = new Point(0, 0); + quad_pts[1] = new Point(transformed.cols(), 0); + quad_pts[2] = new Point(transformed.cols(), transformed.rows()); + quad_pts[3] = new Point(0, transformed.rows()); + + // Get transformation matrix + Mat transmtx = Imgproc.getPerspectiveTransform(new MatOfPoint2f(ccQuad.getPoints()), + new MatOfPoint2f(quad_pts)); + + // Apply perspective transformation + Imgproc.warpPerspective(image, transformed, transmtx, transformed.size()); + + return transformed; + } + + public Mat getExtractedColorChecker() { + return extractedColorChecker; + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/ISaverLoader.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/ISaverLoader.java new file mode 100644 index 0000000..320893c --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/ISaverLoader.java @@ -0,0 +1,14 @@ +package org.wheatdb.seedcounter; + +import java.io.File; +import java.io.IOException; + +import org.xml.sax.SAXException; + +public interface ISaverLoader { + void setData(Object obj); + + void save(File toFile) throws IOException; + + Object load(File fromFile) throws IOException, SAXException; +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/Measurement.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/Measurement.java new file mode 100644 index 0000000..9a27fa0 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/Measurement.java @@ -0,0 +1,385 @@ +package org.wheatdb.seedcounter; + +import java.io.File; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +import org.w3c.dom.Document; + +import ru.delkom07.util.xml.JElement; +import ru.delkom07.util.xml.JXMLRepresentable; + +/** + * Замер. + * Класс представляющий единичный замер зерен с листа бумаги. + * @author Komyshev. + * v.0.3 (C) + */ +public class Measurement { + private String name = ""; // Название серии измерения (опционально) + private Date date; // Дата и время измерения + private List listOfSeedData = new LinkedList(); // Список полученных данных + private File fromSavedImageFile; // измерения получены из сохраненного изображения + private SheetFormat sheetFormat; // формат использованного листа бумаги + private boolean selected = false; // UI selection + private Status status = Status.NONE; + + private int numberOfTrash = -1; + private int numberOfSeeds = -1; + private int numberOfStuckTogether = -1; + private int totalNumberOfStuckTogetherEst = -1; + private int totalNumberOfSeedsEst = -1; + + private boolean objsClassified = false; + + + public enum Status { + NONE, CALCULATED, DELAYED, FAILURE; + + public static Status fromString(String str) { + if(null == str) + return null; + + if(str.equals("NONE")) + return NONE; + + if(str.equals("CALCULATED")) + return CALCULATED; + + if(str.equals("DELAYED")) + return DELAYED; + + if(str.equals("FAILURE")) + return FAILURE; + + return null; + } + } + + + public Measurement(){} + + public Measurement(String name, Date date, File fromSavedImageFile, SheetFormat sheetFormat, Status status) { + this.name = name; + this.date = date; + this.fromSavedImageFile = fromSavedImageFile; + this.sheetFormat = sheetFormat; + this.status = status; + } + + public Measurement(Date date, SeedDataContainer dataItem) { + this.date = date; + listOfSeedData.add(dataItem); + } + + public Measurement(String name, Date date, SeedDataContainer dataItem) { + this(date, dataItem); + this.name = name; + } + + public Measurement(Date date, List listOfData) { + this.date = date; + this.listOfSeedData = listOfData; + } + + public Measurement(String name, Date date, List listOfData) { + this(date, listOfData); + this.name = name; + } + + + /** + * Получить название измерения. + * @return название измерения. + */ + public String getName() { + return name; + } + + + /** + * Задать название измерения. + * @param name - новое название измерения. + */ + public void setName(String name) { + this.name = name; + } + + + /** + * Получить дату измерения. + * @return - дата измерения. + */ + public Date getDate() { + return date; + } + + + /** + * Получить список данных о зернах. + * @return - список данных о зернах. + */ + public List getListOfSeedData() { + return listOfSeedData; + } + + + /** + * Получить список данных о зернах. + * @return - список данных о зернах. + */ + public void setListOfSeedData(List listOfSeedData) { + this.listOfSeedData = listOfSeedData; + } + + + /** + * Добавить в список данных данные о зерне. + * @param dataItem - данные о зерне. + */ + public void addSeedData(SeedDataContainer dataItem) { + listOfSeedData.add(dataItem); + } + + + /** + * Set selection from UI. + * @param selected + */ + public void setSelected(boolean selected) { + this.selected = selected; + } + + + /** + * Returns true if this measurement was selected in UI. + * @return + */ + public boolean isSelected() { + return selected; + } + + + /** + * Set delayed flag to measurement. Such measurement will be calculated later. + * @param + */ + public void setStatus(Status status) { + this.status = status; + } + + + /** + * Returns is delayed flag enabled. + * @return - true if delayed is turn on or false otherwise. + */ + public Status getStatus() { + return status; + } + + + /** + * Set saved local image file name which is used to obtain this measurement. + * It is optional parameter which sets in case if measures was produced in background mode. + * @param file - file object. + */ + public void setFromSavedImageFile(File file) { + this.fromSavedImageFile = file; + } + + + /** + * Get name of saved local image file which is used to obtain this measurement (can be null). + * @return - file object. + */ + public File getFromSavedImageFile() { + return fromSavedImageFile; + } + + + /** + * Get format of sheet used for measuring seeds. + * @return instance of SheetFormat class. + */ + public SheetFormat getSheetFormat() { + return sheetFormat; + } + + + /** + * Set format of sheet used for measuring seeds. + * @param sheetFormat - instance of SheetFormat class. + */ + public void setSheetFormat(SheetFormat sheetFormat) { + this.sheetFormat = sheetFormat; + } + + + public int getNumberOfTrash() { + return numberOfTrash; + } + + public int getNumberOfSeeds() { + return numberOfSeeds; + } + + public int getNumberOfStuckTogether() { + return numberOfStuckTogether; + } + + public int getTotalNumberOfStuckTogetherEst() { + return totalNumberOfStuckTogetherEst; + } + + public int getTotalNumberOfSeedsEst() { + return totalNumberOfSeedsEst; + } + + public boolean isObjsClassified() { + return objsClassified; + } + + public void setNumberOfTrash(int numberOfTrash) { + this.numberOfTrash = numberOfTrash; + + if(numberOfTrash!=-1 && numberOfSeeds!=-1 && numberOfStuckTogether != -1 && totalNumberOfStuckTogetherEst != -1 && totalNumberOfSeedsEst != -1) { + objsClassified = true; + } + } + + public void setNumberOfSeeds(int numberOfSeeds) { + this.numberOfSeeds = numberOfSeeds; + + if(numberOfTrash!=-1 && numberOfSeeds!=-1 && numberOfStuckTogether != -1 && totalNumberOfStuckTogetherEst != -1 && totalNumberOfSeedsEst != -1) { + objsClassified = true; + } + } + + public void setNumberOfStuckTogether(int numberOfStuckTogether) { + this.numberOfStuckTogether = numberOfStuckTogether; + + if(numberOfTrash!=-1 && numberOfSeeds!=-1 && numberOfStuckTogether != -1 && totalNumberOfStuckTogetherEst != -1 && totalNumberOfSeedsEst != -1) { + objsClassified = true; + } + } + + public void setTotalNumberOfStuckTogetherEst(int totalNumberOfStuckTogetherObjs) { + this.totalNumberOfStuckTogetherEst = totalNumberOfStuckTogetherObjs; + + if(numberOfTrash!=-1 && numberOfSeeds!=-1 && numberOfStuckTogether != -1 && totalNumberOfStuckTogetherEst != -1 && totalNumberOfSeedsEst != -1) { + objsClassified = true; + } + } + + public void setTotalNumberOfSeedsEst(int totalNumberOfSeedsEst) { + this.totalNumberOfSeedsEst = totalNumberOfSeedsEst; + + if(numberOfTrash!=-1 && numberOfSeeds!=-1 && numberOfStuckTogether != -1 && totalNumberOfStuckTogetherEst != -1 && totalNumberOfSeedsEst != -1) { + objsClassified = true; + } + } + + + + + + /** + * Представитель JXML используется для экспорта измерений в XML файлы, а также для передачи в базу данных. + * Поля fromSavedImageFile и SheetFormat не экспортируются, т.к. являются метаданными приложения. + * @author Komyshev + */ + public class Representablator implements JXMLRepresentable { + @Override + public JElement getJXMLRepresentation(Document doc) { + JElement root = new JElement(doc, Names.JELEMENT_NAME); + root.setAttribute(Names.NAME_NAME, name); + root.setAttribute(Names.DATE_NAME, new SimpleDateFormat("MMMM d, yyyy HH:mm:ss", Locale.ENGLISH).format(date)); + + for(SeedData seedData : listOfSeedData) { + SeedData.Representablator res = seedData.new Representablator(); + JElement seedDataJEl = res.getJXMLRepresentation(doc); + root.appendChild(seedDataJEl); + } + + return root; + } + + public void recoveryFromJXML(JElement root) { + listOfSeedData = new LinkedList(); + + name = root.getAttribute("name"); + try { + date = new SimpleDateFormat("MMMM d, yyyy HH:mm:ss", Locale.ENGLISH).parse(root.getAttribute("date")); + } catch (ParseException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + List seedDataJEls = root.getAll(SeedData.Representablator.Names.JELEMENT_NAME); + for(JElement seedDataEl : seedDataJEls) { + SeedDataContainer seedData = new SeedDataContainer(); + SeedDataContainer.Representablator repr = seedData.new Representablator(); + repr.recoveryFromJXML(seedDataEl); + + listOfSeedData.add(seedData); + } + } + + public class Names { + public static final String JELEMENT_NAME = "measurement"; + private static final String NAME_NAME = "name"; + private static final String DATE_NAME = "date"; + } + + } + + + /** + * Представитель в TSV используется для экспорта данных в TSV файлы. + * Поля fromSavedImageFile и SheetFormat не экспортируются, т.к. являются метаданными приложения. + * @author Komyshev. + */ + public class TVSRepresentablator { + public String toTSV() { + String caption = "Measurement\t" + name + "\t" + date + "\r\n"; + String result = caption; + + boolean withHeader = true; + for(SeedData seedData : listOfSeedData) { + result = result.concat((seedData.new TVSRepresentablator()).toTSV(withHeader)); + withHeader = false; + } + + return result.concat("\r\n\r\n"); + } + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (obj == null) + return false; + + if (getClass() != obj.getClass()) + return false; + + Measurement other = (Measurement) obj; + if (!name.equals(other.name)) + return false; + + if (!date.equals(other.date)) + return false; + + if (!listOfSeedData.equals(other.listOfSeedData)) + return false; + + return true; + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SeedCounterException.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SeedCounterException.java new file mode 100644 index 0000000..141d965 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SeedCounterException.java @@ -0,0 +1,13 @@ +package org.wheatdb.seedcounter; + +public class SeedCounterException extends Exception { + private static final long serialVersionUID = 1L; + + public SeedCounterException() { + super(); + } + + public SeedCounterException(String mes) { + super(mes); + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SeedData.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SeedData.java new file mode 100644 index 0000000..e821b7a --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SeedData.java @@ -0,0 +1,360 @@ +package org.wheatdb.seedcounter; + +import java.util.ArrayList; +import java.util.List; + +import org.w3c.dom.Document; + +import ru.delkom07.util.xml.JElement; +import ru.delkom07.util.xml.JXMLRepresentable; + +/** + * Единица полученных данных об одном зерне в ходе измерения. + * @author Komyshev. + * v.0.6 (C) + */ +public class SeedData implements WritableData { + private double length; + private double width; + private double height; // (optional) + private double area; + private double[] lengthWidthIntersectPoint = new double[2]; + private double[] massCenter = new double[2]; + private double DFMCTWLIP; //distance from mass center to width length intersect point + + private double circularity; + private double roundness; + private double solidity; + private double rugosity; + private double MIMCCDR; // Maximum Inscribed Circle To Minimum Circumscribed Circle Ratio + private double MICTEACR; // Maximum Inscribed Circle To Equivalent Area Circle Ratio + + public enum ObjType { + TRASH, SEED, STUCKTOGETHER + } + private ObjType objType; + + public SeedData() {} + + public SeedData(double length, double width) { + this.length = length; + this.width = width; + } + + public SeedData(double length, double width, double height) { + this.length = length; + this.width = width; + this.height = height; + } + + public double getLenght() { + return length; + } + + public double getWidth() { + return width; + } + + public double getHeight() { + return height; + } + + public double getArea() { + return area; + } + + public void setArea(double area) { + this.area = area; + } + + public double[] getLengthWidthIntersectPoint() { + return lengthWidthIntersectPoint; + } + + public void setLengthWidthIntersectPoint(double[] lengthWidthIntersectPoint) { + this.lengthWidthIntersectPoint = lengthWidthIntersectPoint; + } + + public double[] getMassCenter() { + return massCenter; + } + + public void setMassCenter(double[] massCenter) { + this.massCenter = massCenter; + } + + public double getDistaceFromMassCenterToWidthLengthIntersectPoint() { + return DFMCTWLIP; + } + + public void setDistaceFromMassCenterToWidthLengthIntersectPoint(double distaceFromMassCenterToWidthLengthIntersectPoint) { + this.DFMCTWLIP = distaceFromMassCenterToWidthLengthIntersectPoint; + } + + + // Additional indexies + // getters + public double getCircularity() { + return circularity; + } + + public double getRoundness() { + return roundness; + } + + public double getSolidity() { + return solidity; + } + + public double getRugosity() { + return rugosity; + } + + public double getMaxInscribedMinCircumscribedCirclesDiametersRatio() { + return MIMCCDR; + } + + public double getMaxInscribedCircleToEqAreaCircleRatio() { + return MICTEACR; + } + + + // setters + public void setCircularityIndex(double circularityIndex) { + this.circularity = circularityIndex; + } + + public void setRoundness(double roundness) { + this.roundness = roundness; + } + + public void setSolidity(double solidity) { + this.solidity = solidity; + } + + public void setRugosity(double rugosity) { + this.rugosity = rugosity; + } + + public void setMaxInscribedMinCircumscribedCirclesDiametersRatio(double maxInscribedMinCircumscribedCirclesDiametersRatio) { + this.MIMCCDR = maxInscribedMinCircumscribedCirclesDiametersRatio; + } + + public void setMaxInscribedCircleToEqAreaCircleRatio(double MaxInscribedCircleToEqAreaCircleRatio) { + this.MICTEACR = MaxInscribedCircleToEqAreaCircleRatio; + } + + public ObjType getObjType() { + return objType; + } + + public void setObjType(ObjType objType) { + this.objType = objType; + } + +// public static List headers() { +// var columnNames = new ArrayList(); +// +// columnNames.add("Length"); +// columnNames.add("Width"); +// columnNames.add("Area"); +// columnNames.add("Circularity"); +// columnNames.add("Roundness"); +// columnNames.add("Rugosity"); +// columnNames.add("Solidity"); +// +// return columnNames; +// } + + /** + * Получить данные в списка в том же порядке, что и заголовки. + * @return список значений в строковом представлении. + */ + @Override + public List data() { + var data = new ArrayList(); + + data.add(Double.toString(length)); + data.add(Double.toString(width)); + data.add(Double.toString(area)); + data.add(Double.toString(circularity)); + data.add(Double.toString(roundness)); + data.add(Double.toString(rugosity)); + data.add(Double.toString(solidity)); + + return data; + } + + @Override + public String toString(String separator) { + return length + separator + width + separator + area + separator + circularity + separator + roundness + separator + rugosity + separator + solidity + separator; + } + + + /** + * Представитель в JXML. Используется для экспорта данных в файлы формата XML, а также для передачи данных в базу данных. + * @author Komyshev. + */ + class Representablator implements JXMLRepresentable { + @Override + public JElement getJXMLRepresentation(Document doc) { + JElement root = new JElement(doc, Names.JELEMENT_NAME); + root.setAttribute(Names.LENGTH_NAME, Double.toString(length)); + root.setAttribute(Names.WIDTH_NAME, Double.toString(width)); + //root.setAttribute(Names.HEIGHT_NAME, Double.toString(height)); + root.setAttribute(Names.AREA_NAME, Double.toString(area)); + root.setAttribute(Names.DFMCTWLIP_NAME, Double.toString(DFMCTWLIP)); + + root.setAttribute(Names.CIRCULARITY_INDEX_NAME, Double.toString(circularity)); + root.setAttribute(Names.ROUNDNESS_NAME, Double.toString(roundness)); + root.setAttribute(Names.SOLIDITY_NAME, Double.toString(solidity)); + root.setAttribute(Names.RUGOSITY_NAME, Double.toString(rugosity)); + root.setAttribute(Names.MIMCCDR_NAME, Double.toString(MIMCCDR)); + root.setAttribute(Names.MICTEACR_NAME, Double.toString(MICTEACR)); + + JElement lengthWidthIntersectPointJEl = new JElement(doc, Names.LENGTH_WIDTH_INTERSECT_POINT_NAME); + root.appendChild(lengthWidthIntersectPointJEl); + lengthWidthIntersectPointJEl.setAttribute(Names.X_COORDINATE_NAME, Double.toString(lengthWidthIntersectPoint[0])); + lengthWidthIntersectPointJEl.setAttribute(Names.Y_COORDINATE_NAME, Double.toString(lengthWidthIntersectPoint[1])); + + JElement massCenterPointJEl = new JElement(doc, Names.MASS_CENTER_NAME); + root.appendChild(massCenterPointJEl); + massCenterPointJEl.setAttribute(Names.X_COORDINATE_NAME, Double.toString(massCenter[0])); + massCenterPointJEl.setAttribute(Names.Y_COORDINATE_NAME, Double.toString(massCenter[1])); + + return root; + } + + @Override + public void recoveryFromJXML(JElement root) { + length = Double.parseDouble(root.getAttribute(Names.LENGTH_NAME)); + width = Double.parseDouble(root.getAttribute(Names.WIDTH_NAME)); + //height = Double.parseDouble(root.getAttribute(Names.HEIGHT_NAME)); + area = Double.parseDouble(root.getAttribute(Names.AREA_NAME)); + DFMCTWLIP = Double.parseDouble(root.getAttribute(Names.DFMCTWLIP_NAME)); + + circularity = Double.parseDouble(root.getAttribute(Names.CIRCULARITY_INDEX_NAME)); + roundness = Double.parseDouble(root.getAttribute(Names.ROUNDNESS_NAME)); + solidity = Double.parseDouble(root.getAttribute(Names.SOLIDITY_NAME)); + rugosity = Double.parseDouble(root.getAttribute(Names.RUGOSITY_NAME)); + MIMCCDR = Double.parseDouble(root.getAttribute(Names.MIMCCDR_NAME)); + MICTEACR = Double.parseDouble(root.getAttribute(Names.MICTEACR_NAME)); + + JElement lengthWidthIntersectPointJEl = root.get(Names.LENGTH_WIDTH_INTERSECT_POINT_NAME); + lengthWidthIntersectPoint[0] = Double.parseDouble(lengthWidthIntersectPointJEl.getAttribute(Names.X_COORDINATE_NAME)); + lengthWidthIntersectPoint[1] = Double.parseDouble(lengthWidthIntersectPointJEl.getAttribute(Names.Y_COORDINATE_NAME)); + + JElement massCenterPointJEl = root.get(Names.MASS_CENTER_NAME); + massCenter[0] = Double.parseDouble(massCenterPointJEl.getAttribute(Names.X_COORDINATE_NAME)); + massCenter[1] = Double.parseDouble(massCenterPointJEl.getAttribute(Names.Y_COORDINATE_NAME)); + } + + class Names { + public static final String JELEMENT_NAME = "seedData"; + private static final String LENGTH_NAME = "length"; + private static final String WIDTH_NAME = "width"; + //private static final String HEIGHT_NAME = "height"; + private static final String AREA_NAME = "area"; + private static final String LENGTH_WIDTH_INTERSECT_POINT_NAME = "lengthWidthIntersectPoint"; + private static final String MASS_CENTER_NAME = "massCenter"; + private static final String DFMCTWLIP_NAME = "dfmctwlip"; + private static final String X_COORDINATE_NAME = "x"; + private static final String Y_COORDINATE_NAME = "y"; + + private static final String CIRCULARITY_INDEX_NAME = "circularityIndex"; + private static final String ROUNDNESS_NAME = "roundness"; + private static final String SOLIDITY_NAME = "solidity"; + private static final String RUGOSITY_NAME = "rugosity"; + private static final String MIMCCDR_NAME = "mimccdr"; + private static final String MICTEACR_NAME = "micteacr"; + } + } + + + /** + * Представитель в TSV. Используется для экспорта данных в файлы формата TSV. + * @author Komyshev. + */ + class TVSRepresentablator { + private String header = "Length\tWidth\tArea\tCircularity Index\tRoundness\tSolidity\tRugosity\t" + + "Maximum inscribed circle to minimum circumscribed circle ratio\tMaximum inscribed circle to equivalent area circle ratio\t"+ + "Main axises intersection point X\tMain axises intersection point Y\tMass center X\tMass center Y\tDistance from mass center point to main axises intersection point\n"; + + public String toTSV(boolean first) { + String output = length+"\t"+width+"\t"+area+"\t"+ + circularity+"\t"+roundness+"\t"+solidity+"\t"+rugosity+"\t"+MIMCCDR+"\t"+MICTEACR+"\t"+ + lengthWidthIntersectPoint[0]+"\t"+lengthWidthIntersectPoint[1]+"\t"+ + massCenter[0]+"\t"+massCenter[1]+"\t"+DFMCTWLIP+"\r\n"; + + if(first) { + return header.concat(output); + } else { + return output; + } + } + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (obj == null) + return false; + + if (getClass() != obj.getClass()) + return false; + + SeedData other = (SeedData) obj; + if (length != other.length) + return false; + + if (width != other.width) + return false; + + //if (height != other.height) + // return false; + + if (area != other.area) + return false; + + if(circularity != other.circularity) { + return false; + } + + if(roundness != other.roundness) { + return false; + } + + if(rugosity != other.rugosity) { + return false; + } + + if(solidity != other.solidity) { + return false; + } + + if(MIMCCDR != other.MIMCCDR) { + return false; + } + + if(MICTEACR != other.MICTEACR) { + return false; + } + + if (lengthWidthIntersectPoint[0] != other.lengthWidthIntersectPoint[0] + || lengthWidthIntersectPoint[1] != other.lengthWidthIntersectPoint[1]) + return false; + + if (massCenter[0] != other.massCenter[0] + || massCenter[1] != other.massCenter[1]) + return false; + + if (DFMCTWLIP != other.DFMCTWLIP) + return false; + + return true; + } + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SeedDataContainer.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SeedDataContainer.java new file mode 100644 index 0000000..41f2ff1 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SeedDataContainer.java @@ -0,0 +1,44 @@ +package org.wheatdb.seedcounter; + +import org.opencv.core.MatOfPoint; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.imgproc.Imgproc; +import org.wheatdb.seedcounter.processor.PlanarObject; + +public class SeedDataContainer extends SeedData implements PlanarObject { + private MatOfPoint contour = null; + + public SeedDataContainer() { + super(); + } + + public SeedDataContainer(MatOfPoint contour, double length, double width) { + super(length, width); + this.contour = contour; + } + + public SeedDataContainer(double length, double width) { + super(length, width); + } + + public SeedDataContainer(double length, double width, double height) { + super(length, width, height); + } + + @Override + public MatOfPoint getContour() { + return contour; + } + + public MatOfPoint2f getContourApprox() { + Point[] contourArr = contour.toArray(); + MatOfPoint2f contour2f = new MatOfPoint2f(contourArr); + double perimeter = Imgproc.arcLength(contour2f, true); + + MatOfPoint2f approx2f = new MatOfPoint2f(); + Imgproc.approxPolyDP(contour2f, approx2f, perimeter*0.02, true); + + return approx2f; + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SheetFormat.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SheetFormat.java new file mode 100644 index 0000000..f82dac5 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/SheetFormat.java @@ -0,0 +1,129 @@ +package org.wheatdb.seedcounter; + +import java.util.StringTokenizer; + +import org.opencv.core.Size; + + +/** + * Formats of sheet for detecting seeds. + * @author Komyshev + * v.1.0 + */ +public enum SheetFormat { + A2, A3, A4, A5, // International standard + Letter, Legal, // USA standard + B4, B5, B6, // Japanese standard + USER; // User settings + + Size userSize = new Size(0, 0); + + + /** + * Returns size of this sheet format. + * @return size of sheet format represented by this object. + */ + public Size getSize() { + switch(this) { + case A2: return new Size(420, 594); + case A3: return new Size(297, 420); + case A4: return new Size(210, 297); + case A5: return new Size(148, 210); + + case Letter: return new Size(216, 279); + case Legal: return new Size(216, 356); + + case B4: return new Size(257, 364); + case B5: return new Size(182, 257); + case B6: return new Size(128, 182); + case USER: { + if(null==userSize) + throw new IllegalArgumentException("User sheet format must be with width and height arguments."); + return userSize; + } + } + + return null; + } + + public void setUserSize(Size userSize) { + if(!this.equals(USER)) + throw new IllegalArgumentException("Only user sheet may be setted with width and height."); + this.userSize = userSize; + } + + /** + * Returns big side size of this sheet format. + * @return the big side size of sheet format represented by this object. + */ + public double getBigSideSize() { + Size size = getSize(); + return size.height > size.width ? size.height: size.width; + } + + /** + * Returns small side size of this sheet format. + * @return the small side size of sheet format represented by this object. + */ + public double getSmallSideSize() { + Size size = getSize(); + return size.height > size.width ? size.width : size.height; + } + + /** + * Returns parsed instance of SheedFormat from string. + * @param string - source string. + * @return parsed instance of SheedFormat from string. + */ + public static SheetFormat fromString(String string) { + if(null == string) + return null; + + if("" == string) + return null; + + if(A2.toString().equals(string)) + return SheetFormat.A2; + if(A3.toString().equals(string)) + return SheetFormat.A3; + if(A4.toString().equals(string)) + return SheetFormat.A4; + if(A5.toString().equals(string)) + return SheetFormat.A5; + + if(Letter.toString().equals(string)) + return SheetFormat.Letter; + if(Legal.toString().equals(string)) + return SheetFormat.Legal; + + if(B4.toString().equals(string)) + return SheetFormat.B4; + if(B5.toString().equals(string)) + return SheetFormat.B5; + if(B6.toString().equals(string)) + return SheetFormat.B6; + + StringTokenizer st = new StringTokenizer(string); + if(st.hasMoreTokens()) + if(st.nextToken().equals("USER")) { + double width = Double.parseDouble(st.nextToken()); + double height = Double.parseDouble(st.nextToken()); + + SheetFormat user = SheetFormat.USER; + user.setUserSize(new Size(height, width)); + return user; + } + + return null; + } + + @Override + public String toString() { + if(this==USER) { + String sizeStr = ""; + if(null!=userSize) sizeStr = userSize.height+" "+userSize.width; + return "USER "+ sizeStr; + } + return super.toString(); + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/WritableData.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/WritableData.java new file mode 100644 index 0000000..b603a76 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/WritableData.java @@ -0,0 +1,23 @@ +package org.wheatdb.seedcounter; + +import java.util.List; + +/** + * Данные, предназначенные для сохранения в файлы. + */ +public interface WritableData { + + /** + * Получить данные в виде списка. + * @return список значений в строковом представлении. + */ + public List data(); + + /** + * Получить данные в виде строки с разделителями. + * @param separator - раделители колонок в файлах (точка с запятой, знак табуляции и т.п.). + * @return строковое представление данных. + */ + public String toString(String separator); + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/XMLMapSaverLoader.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/XMLMapSaverLoader.java new file mode 100644 index 0000000..cd4d1cb --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/XMLMapSaverLoader.java @@ -0,0 +1,61 @@ +package org.wheatdb.seedcounter; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import ru.delkom07.util.xml.JElement; +import ru.delkom07.util.xml.XMLDOMHandler; + +public class XMLMapSaverLoader implements ISaverLoader { + private Map data; + + @SuppressWarnings("unchecked") + @Override + public void setData(Object data) { + this.data = (Map) data; + } + + @Override + public void save(File toFile) throws IOException { + Document document = XMLDOMHandler.newDocument(); + JElement root = new JElement(document, toFile.getName()); + document.appendChild(root.getElement()); + + for(Entry entry : data.entrySet()) { + String name = entry.getKey(); + String value = entry.getValue(); + + JElement element = new JElement(document, name); + element.setTextContent(value); + root.appendChild(element); + } + + XMLDOMHandler.writeTree(document, toFile); + } + + @Override + public Map load(File fromFile) throws IOException, SAXException { + Map data = new HashMap(); + Document document; + + document = XMLDOMHandler.parse(fromFile); + JElement root = new JElement(document.getDocumentElement()); + List elements = root.getChildJElements(); + + for(JElement element : elements) { + String name = element.getName(); + String value = element.getTextContent(); + + data.put(name, value); + } + + return data; + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/XMLSaverLoader.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/XMLSaverLoader.java new file mode 100644 index 0000000..6f3d982 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/XMLSaverLoader.java @@ -0,0 +1,43 @@ +package org.wheatdb.seedcounter; + +import java.io.File; +import java.io.IOException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import ru.delkom07.util.xml.JElement; +import ru.delkom07.util.xml.XMLDOMHandler; + +public class XMLSaverLoader implements ISaverLoader { + JElement root; + + @Override + public void setData(Object obj) { + root = (JElement) obj; + } + + @Override + public void save(File toFile) throws IOException { + XMLDOMHandler.writeTree(root.getElement().getOwnerDocument(), toFile); + } + + @Override + public JElement load(File fromFile) { + Document document = null; + try { + document = XMLDOMHandler.parse(fromFile); + } catch (SAXException e) { + return null; + } catch (IOException e) { + return null; + } + + Element root = document.getDocumentElement(); + if(null!=root) + return new JElement(root); + else + return null; + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/AppPrint.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/AppPrint.java new file mode 100644 index 0000000..a98f91c --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/AppPrint.java @@ -0,0 +1,19 @@ +package org.wheatdb.seedcounter.desktop; + +import java.io.File; +import java.io.IOException; + +public class AppPrint { + + public static void print(String str) { + System.out.println(str); + } + + public static void eprint(String str) { + System.err.println(str); + } + + public static void printFilePath(File file) throws IOException { + System.out.println(file.getCanonicalPath()); + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ColorCorrection.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ColorCorrection.java new file mode 100644 index 0000000..ce09aba --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ColorCorrection.java @@ -0,0 +1,65 @@ +package org.wheatdb.seedcounter.desktop; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.imgcodecs.Imgcodecs; +import org.wheatdb.imgproc.colorchecker.MyColorChecker; + +import ru.delkom07.fileutils.FileUtilities; +import ru.delkom07.util.Pair; +import ru.delkom07.workspaces.SimpleWorkspace.OpUnit; +import ru.delkom07.workspaces.StatusWorkspace; + +public class ColorCorrection { + + public static void main(String[] args) throws IOException { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + + File inputDir = new File(args[0]); + File outputDir = new File(args[1]); + + StatusWorkspace workspace = new StatusWorkspace(inputDir, outputDir, true, FileUtilities.jpgImageFilter, null); + List inputAndOutputs = workspace.getRawOpUnits(); + + File dataFile = new File(outputDir, "data.csv"); + PrintWriter printWriter = new PrintWriter(dataFile); + printWriter.println("File name;transformationDeviance"); + + System.out.println("Total imgs amount: " + inputAndOutputs.size() + "\n"); + + int count = 1; + for(OpUnit inputAndOutput : inputAndOutputs) { + File inputFile = inputAndOutput.getIn(); + File outputFile = inputAndOutput.getOut(); + + try { + System.out.print(count++ + " " + inputFile.getCanonicalPath() + "... "); + + Mat inputImg = Imgcodecs.imread(inputFile.getCanonicalPath()); + + MyColorChecker myColorChecker = new MyColorChecker(inputImg); + + Pair calibratedPair = myColorChecker.calibrateImg(); + + Mat calibrated = calibratedPair.getLeft(); + double transformationDeviance = calibratedPair.getRight(); + + System.out.println("D: " + transformationDeviance); + printWriter.println(inputFile.getCanonicalPath() + ";" + transformationDeviance); + printWriter.flush(); + + Imgcodecs.imwrite(outputFile.getCanonicalPath(), calibrated); + } catch(Exception e) { + System.err.println("error. File was skipped."); + } + } + + printWriter.close(); + } + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ColorSpace.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ColorSpace.java new file mode 100644 index 0000000..c4a847b --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ColorSpace.java @@ -0,0 +1,5 @@ +package org.wheatdb.seedcounter.desktop; + +public enum ColorSpace { + RGB, HSV, Lab, YCrCb; +} \ No newline at end of file diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/DesktopMain.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/DesktopMain.java new file mode 100644 index 0000000..d89dc4a --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/DesktopMain.java @@ -0,0 +1,220 @@ +package org.wheatdb.seedcounter.desktop; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.io.FilenameUtils; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; +import org.wheatdb.imgproc.colorchecker.MyColorChecker; +import org.wheatdb.seedcounter.SheetFormat; +import org.wheatdb.seedcounter.processor.DetectionProcessor; +import org.wheatdb.seedcounter.processor.PlanarObject; +import org.wheatdb.seedcounter.processor.SeedCounterProcessorException; +import org.wheatdb.seedcounter.processor.subjects.Quad; +import org.wheatdb.seedcounter.server.DefaultFilesAndDirs; + +import ru.delkom07.util.ArgOption; +import ru.delkom07.util.CommandRepresentation; +import ru.delkom07.util.IncorrectCmdException; +import ru.delkom07.util.Pair; + +/** + * It is obsolete?? Check it! + * Main class of test application. + */ +public class DesktopMain { + static class ImageFilter implements java.io.FileFilter { + @Override + public boolean accept(File pathname) { + if(pathname.getName().matches(".+\\.(png|PNG|jpg|JPG|bmp|BMP)")) + return true; + return false; + } + } + + + public static void main(String[] args) { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + + try { + // processing of command line + List optMap = new ArrayList(2); + optMap.add(new ArgOption("--out", "-o", false)); + optMap.add(new ArgOption("--help", "-h", true)); + + optMap.add(new ArgOption("--colorchecker", "-cc", true)); + optMap.add(new ArgOption("--calibrate", "-cl", true)); + optMap.add(new ArgOption("--withoutsheet", "-wh", true)); + + CommandRepresentation cmd = + new CommandRepresentation(args, optMap, optMap.size(), 0, 2000, 0); + + // command options + if(cmd.containsOptionByName("--help")) + usage(""); + + File outputDir = null; + if(cmd.containsOptionByName("--out")) { + + outputDir = new File(cmd.getOptionByName("--out").getValue()); + + if(!outputDir.isDirectory()) + throw new IncorrectCmdException("Option '--out' isn't a path to directory."); + } else { + outputDir = DefaultFilesAndDirs.outputDir; + + if(outputDir.isFile()) { + throw new IncorrectCmdException("Default output directory is existing file."); + } + + if(!outputDir.exists()) { + outputDir.mkdir(); + } + } + + + // preparing input files + List inputFiles = new LinkedList(); + + String[] appArg = cmd.getAcceptedArgs(); + if(appArg.length != 0) { + for(String inputPathStr : appArg) { + File inputPath = new File(inputPathStr); + if(!inputPath.exists()) + throw new IncorrectCmdException("Input file doesn't exists."); + + if(inputPath.isDirectory()) { + File[] files = inputPath.listFiles(new DesktopMain.ImageFilter()); + + for(File f : files) { + inputFiles.add(f); + } + + } else { + inputFiles.add(inputPath); + } + } + } else { + File[] files = DefaultFilesAndDirs.inputDir.listFiles(new DesktopMain.ImageFilter()); + + for(File f : files) { + inputFiles.add(f); + } + } + + + // processing input files + DetectionProcessor detectProcessor = new DetectionProcessor(SheetFormat.A4); + for(File inputImg : inputFiles) { + Mat srcImg = Imgcodecs.imread(inputImg.getAbsolutePath()); + if(srcImg.empty()) + throw new IncorrectCmdException("Input file is not image."); + + Mat region = srcImg; + + if(!cmd.containsOptionByName("--withoutsheet")) { + Quad quad = detectProcessor.detectQuad(srcImg); + if(null==quad) { + System.out.print("Input file:" + inputImg.getName()+". "); + System.out.println("Sheet isn't found. Skipping image."); + + continue; + } + region = detectProcessor.getTransformedField(srcImg, quad); + } + + List containers; + + // if is colorchecker + if(cmd.containsOptionByName("--colorchecker")) { + MyColorChecker myColorChecker = new MyColorChecker(region); + + // fill ColorChecker + myColorChecker.fillColorChecker(); + + if(cmd.containsOptionByName("--calibrate")) { + Pair calibratedPair = myColorChecker.calibrateImg(); + region = calibratedPair.getLeft(); + Double transformationDeviance = calibratedPair.getRight(); + + System.out.println("# Transformation deviance: " + transformationDeviance.toString()); + } + + containers = detectProcessor.detectSeedContours(region.clone(), myColorChecker.getPixPerMM()); + } else { + containers = detectProcessor.detectSeedContours(region.clone()); + } + + System.out.println("Seed count: " + containers.size()); + + String base = FilenameUtils.getBaseName(inputImg.getName()); + String extention = "tsv"; + File outputImgFile = new File(outputDir, base + "." + extention); + + saveResults(outputImgFile, containers); + + File outputFile = new File(outputDir, "output_" + inputImg.getName().replace(".jpg", ".png")); + drawContoursWithRectAndNum(region, containers); + Imgcodecs.imwrite(outputFile.getCanonicalPath(), region); + } + + } catch (IncorrectCmdException e) { + System.err.println("Error: "+e.getMessage()); + usage(e.getMessage()); + } catch (IOException e) { + System.err.println("Error: "+e.getMessage()); + System.out.println("IO error. The program will be terminated"); + System.exit(0); + } catch (SeedCounterProcessorException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + + + private static void drawContoursWithRectAndNum(Mat img, List containers) { + for(int i=0; i containers) throws IOException { + PrintWriter writer = new PrintWriter(outputDataFile, "UTF-8"); + + int count = 1; + writer.println("Number" + " " + "Length" + " " + "Width" + " " + "Area" + " " + + "Circularity" + " " + "Roundness" + " " + "Rugosity" + " " + "Solidity"); + for(int i=0; i optMap = new ArrayList(2); + optMap.add(new ArgOption("--help", "-h", true)); // print help + optMap.add(new ArgOption("--out", "-o", false)); // set output directory + + optMap.add(new ArgOption("--colorchecker", "-cc", true)); // image contains ColorChecker + optMap.add(new ArgOption("--calibrate", "-cl", true)); // do color calibration with ColorChecker + optMap.add(new ArgOption("--withoutsheet", "-ws", true)); // background is solid. standard sheet of paper is not used for scale definition. + + optMap.add(new ArgOption("--colordescriptors", "-cd", true)); // calculate color descriptors + optMap.add(new ArgOption("--texturedescriptors", "-td", true)); // calculate texture descriptors + optMap.add(new ArgOption("--draw", "-dr", true)); // draw additional output images + optMap.add(new ArgOption("--markdescriptor", "-md", false)); // mark HSV color descriptor on output image + optMap.add(new ArgOption("--debug", "-dg", true)); // enable debug mode + + return new CommandRepresentation(args, optMap, optMap.size(), 0, 2000, 0); + } + + + private static RunConfiguration initAppConfiguration(String[] args) throws IncorrectCmdException { + SpringApplication.run(DesktopMainWithColorDescriptorsOneThread.class, args); + @SuppressWarnings("resource") + ApplicationContext context = new ClassPathXmlApplicationContext("context.xml"); + + RunConfiguration config = (RunConfiguration) context.getBean("config"); + CommandRepresentation cmd = init(args); + handleOptions(cmd, config); + + config.imageHandler = (ImageHandler) context.getBean("imageHandler"); + + if(config.colordescriptors && !config.texturedescriptors) { + config.imageHandler.setResultSaver((ResultSaver) context.getBean("colorResultSaver")); + } else + if(!config.colordescriptors && config.texturedescriptors) { + config.imageHandler.setResultSaver((ResultSaver) context.getBean("textureResultSaver")); + } else + if(config.colordescriptors && config.texturedescriptors) { + config.imageHandler.setResultSaver((ResultSaver) context.getBean("colorTextureResultSaver")); + } + + // Additional info writing to output table + config.info = (AddInfo) context.getBean("simpleAddInfo"); + config.imageHandler.getResultSaver().setInfo(config.info); + + return config; + } + + + public static void main(String[] args) { + OpenCV.loadLocally(); + + long time = System.currentTimeMillis(); + + try { + var config = initAppConfiguration(args); + + String base = "output"; + String extention = "tsv"; + File outputDataFile = new File(config.outputDir, base + "." + extention); + + PrintWriter writer = new PrintWriter(outputDataFile, "UTF-8"); + + ResultSaver resultSaver = config.imageHandler.getResultSaver(); + resultSaver.writeFileHead(writer); + writer.println(); + + StatusWorkspace workspace = new StatusWorkspace(config.inputDir, config.outputDir, true, null, null); + List opUnits = workspace.getRawOpUnits(); + + for(OpUnit unit : opUnits) { + File inFile = unit.getIn(); + File outFile = unit.getOut(); + + printFilePath(inFile); + + try { + resultSaver.setInfo(config.info.from(inFile)); + + config.imageHandler.handleImage(inFile, outFile, writer); + + print(inFile.getParent() + "/" + inFile.getName() + " done."); + } catch(QuadNotFoundException e) { + eprint("Input file:" + inFile.getName()); + eprint("Error: " + e.getMessage()); + eprint("Sheet isn't found. Skipping image..."); + } catch (SeedCounterProcessorException e) { + eprint("Input file:" + inFile.getName()); + eprint("Error: " + e.getMessage()); + eprint("Inner error. Skipping image..."); + } catch (IOException e) { + eprint("Input file:" + inFile.getName()); + eprint("Error: " + e.getMessage()); + eprint("IO error. Skipping image..."); + } catch (SeedCounterException e) { + eprint("Input file:" + inFile.getName()); + eprint("Error: " + e.getMessage()); + eprint("Skipping image..."); + } catch (Exception e) { + eprint("Input file:" + inFile.getName()); + eprint("Unknow exception: " + e.getMessage()); + e.printStackTrace(); + eprint("Skipping image..."); + } finally { + workspace.saveProcessedOpUnit(unit); + } + } + + writer.close(); + System.out.println("Computation time: " + Integer.toString((int)(System.currentTimeMillis() - time)/1000)); + System.exit(0); // STATUS - ok + + } catch (IncorrectCmdException e) { + eprint("Error: " + e.getMessage()); + eprint("Command line error. The program will be terminated"); + usage(e.getMessage()); + System.exit(-1); + } catch (IOException e) { + eprint("Error: " + e.getMessage()); + eprint("IO error. The program will be terminated"); + System.exit(1); + } + } + + + private static void usage(String message) { + System.out.println(message + "\n" + Messages.usage); + System.exit(0); + } +} \ No newline at end of file diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/Display.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/Display.java new file mode 100644 index 0000000..21859c1 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/Display.java @@ -0,0 +1,87 @@ +package org.wheatdb.seedcounter.desktop; + + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; +import javax.swing.JFrame; + +import org.opencv.core.Mat; +import org.opencv.core.MatOfByte; +import org.opencv.imgcodecs.Imgcodecs; + +public class Display extends JFrame { + private static final long serialVersionUID = 1L; + + private static int screenHeight = 1030; + private static int screenWidth = 1284; + + private static int screenUpPadding = 100; + private static int screenSidePaddint = 30; + + private int windowHeight = screenHeight-screenUpPadding*2; + private int windowWidth = screenWidth-screenSidePaddint*2; + + private BufferedImage image = null; + private static int imagePadding = 30; + private int imageHeight; + private int imageWidth; + + + public Display() { + super("Display..."); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + setSize(windowWidth, windowHeight); + setLocation(screenSidePaddint, screenUpPadding); + setVisible(true); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + //Graphics2D graphics = (Graphics2D) g; + + if(null!=image) { + double heightResize = ((getHeight()-imagePadding*2)/(double)image.getHeight()); + double widthResize = ((getWidth()-imagePadding*2)/(double)image.getWidth()); + double resize = heightResize < widthResize ? heightResize : widthResize; + + imageHeight = (int) (image.getHeight()*resize); + imageWidth = (int) (image.getWidth()*resize); + + g.drawImage(image, imagePadding, imagePadding*2, imageWidth, imageHeight, + 0,0,image.getWidth(),image.getHeight(), new Color(10,255,0), null); + } + } + + private static BufferedImage matToImage(Mat mat) { + MatOfByte bytemat = new MatOfByte(); + Imgcodecs.imencode(".jpg", mat, bytemat); + byte[] bytes = bytemat.toArray(); + InputStream in = new ByteArrayInputStream(bytes); + BufferedImage img = null; + try { + img = ImageIO.read(in); + } catch (IOException e) { + e.printStackTrace(); + } + return img; + } + + public void setImage(Mat img) { + image = matToImage(img); + repaint(); + } + + public void setImage(BufferedImage img) { + image = img; + repaint(); + } + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/JSONSaver.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/JSONSaver.java new file mode 100644 index 0000000..e3b5fbc --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/JSONSaver.java @@ -0,0 +1,73 @@ +package org.wheatdb.seedcounter.desktop; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +import org.apache.commons.io.FilenameUtils; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.opencv.core.Mat; +import org.opencv.core.Point; +import org.opencv.imgcodecs.Imgcodecs; +import org.wheatdb.seedcounter.SeedDataContainer; +import org.wheatdb.seedcounter.processor.PlanarObject; + +public class JSONSaver { + + public void saveInJSON(Mat region, List containers, File inFile, File outFile) throws IOException { + PrintWriter jsonWriter = new PrintWriter(new File(outFile.getParent(), FilenameUtils.getBaseName(outFile.getName()) + ".json"), "UTF-8"); + + JSONObject jsonObj = new JSONObject(); + JSONArray shapes = new JSONArray(); + + jsonObj.put("version", "4.0.0"); + jsonObj.put("flags", new JSONObject()); + jsonObj.put("shapes", shapes); + + jsonObj.put("imagePath", outFile.getName()); + jsonObj.put("imageData", null); + jsonObj.put("imageHeight", region.height()); + jsonObj.put("imageWidth", region.width()); + + for(PlanarObject container : containers) { + + JSONObject fly = new JSONObject(); + fly.put("label", "fly"); + fly.put("group_id", null); + fly.put("shape_type", "polygon"); + fly.put("flags", new JSONObject()); + + JSONArray points = new JSONArray(); + Point[] contourPoints = container.getContourApprox().toArray(); + + for(Point point : contourPoints) { + + + + JSONArray jsonPoint = new JSONArray(); + jsonPoint.add(point.x); + jsonPoint.add(point.y); + + points.add(jsonPoint); + } + + fly.put("points", points); + + shapes.add(fly); + } + + try { + jsonObj.writeJSONString(jsonWriter); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + jsonWriter.close(); + Imgcodecs.imwrite(outFile.getCanonicalPath(), region); + } + + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/Messages.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/Messages.java new file mode 100644 index 0000000..71c65a6 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/Messages.java @@ -0,0 +1,16 @@ +package org.wheatdb.seedcounter.desktop; + +public class Messages { + + public static final String usage = "" + + "Usage: java -jar seedCounter.jar [--out outputDir] inputImgsDir \n\n" + + + "-o, --out set output directory\n" + + "-cc, --colorchecker image contains ColorChecker\n" + + "-cl, --calibrate do color calibration with ColorChecker\n" + + "-ws, --withoutsheet background is solid. standard sheet of paper is not used for scale definition\n" + + "-cd, --colordescriptors calculate color descriptors\n" + + "-dr, --draw draw additional output images\n" + + "-md, --markdescriptor mark HSV color descriptor on output image\n"; + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/RunConfiguration.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/RunConfiguration.java new file mode 100644 index 0000000..da85092 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/RunConfiguration.java @@ -0,0 +1,29 @@ +package org.wheatdb.seedcounter.desktop; + +import java.io.File; + +import org.wheatdb.seedcounter.desktop.ihandlers.AddInfo; +import org.wheatdb.seedcounter.desktop.ihandlers.ImageHandler; + +public class RunConfiguration { + // options: + public boolean colorchecker = false; + public boolean calibrate = false; + public boolean withoutsheet = false; + public boolean colordescriptors = false; + public boolean texturedescriptors = false; + public boolean draw = false; + public boolean markdescriptor = false; + public boolean DEBUG_MODE = false; + + // Mark GCH Descriptor + public ColorSpace colorSpace = null; + public int markGCHDimension = -1; + public int markGCHBinNumber = -1; + + public File inputDir; + public File outputDir; + + public ImageHandler imageHandler; + public AddInfo info; +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/SeedCounterWCDThread.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/SeedCounterWCDThread.java new file mode 100644 index 0000000..84e44e3 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/SeedCounterWCDThread.java @@ -0,0 +1,292 @@ +package org.wheatdb.seedcounter.desktop; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; + +import org.apache.commons.io.FilenameUtils; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; +import org.wheatdb.imgproc.colorchecker.MyColorChecker; +import org.wheatdb.seedcounter.SheetFormat; +import org.wheatdb.seedcounter.processor.DetectionProcessor; +import org.wheatdb.seedcounter.processor.PlanarObject; +import org.wheatdb.seedcounter.processor.SeedCounterProcessorException; +import org.wheatdb.seedcounter.processor.subjects.Quad; + +import ru.delkom07.improc.color.descriptors.ColorLayoutDescriptor; +import ru.delkom07.improc.color.descriptors.DominantColorDescriptor; +import ru.delkom07.improc.color.descriptors.GlobalColorHistogram; +import ru.delkom07.improc.color.descriptors.MeanColorDescriptor; +import ru.delkom07.util.CommandRepresentation; +import ru.delkom07.util.IncorrectCmdException; +import ru.delkom07.util.Pair; + +/** + * It is obsolete?? Check it! + * @author Komyshev + * + */ +public class SeedCounterWCDThread implements Runnable { + private static final String SEPARATOR = " "; + + // processing input files + private DetectionProcessor detectProcessor = new DetectionProcessor(SheetFormat.A4); + private List> inputAndOutputs; + private PrintWriter writer; + private CommandRepresentation cmd; + + ExecutorService service; + + public SeedCounterWCDThread(ExecutorService service, List> inputAndOutputs, CommandRepresentation cmd, PrintWriter writer) { + this.service = service; + this.inputAndOutputs = inputAndOutputs; + this.cmd = cmd; + this.writer = writer; + } + + @Override + public void run() { + for(Pair pair : inputAndOutputs) { + File inFile = pair.getLeft(); + File outFile = pair.getRight(); + + String group = inFile.getParentFile().getName(); + String[] splitedSampleDirName = inFile.getParentFile().getParentFile().getName().split(" "); + String sampleName; + String year; + if(splitedSampleDirName.length == 3) { + sampleName = splitedSampleDirName[0] + "_" + splitedSampleDirName[1]; + year = splitedSampleDirName[2]; + } else + if(splitedSampleDirName.length == 2){ + sampleName = splitedSampleDirName[0] + "_" + splitedSampleDirName[1]; + year = "-"; + } else { + sampleName = splitedSampleDirName[0]; + year = "-"; + } + + String mixing = FilenameUtils.getBaseName(inFile.getName()).split("_")[0]; + String duplicate = FilenameUtils.getBaseName(inFile.getName()).split("_")[1]; + + System.out.println(inFile.getParent() + "/" + inFile.getName()); + + try { + + Mat srcImg = Imgcodecs.imread(inFile.getAbsolutePath()); + if(srcImg.empty()) + throw new IncorrectCmdException("Input file is not image."); + + Mat region = srcImg; + + if(!cmd.containsOptionByName("--withoutsheet")) { + Quad quad = detectProcessor.detectQuad(srcImg); + if(null==quad) { + System.out.print("Input file:" + inFile.getName()+". "); + System.out.println("Sheet isn't found. Skipping image."); + + continue; + } + region = detectProcessor.getTransformedField(srcImg, quad); + } + + List containers; + + // if is colorchecker + Double transformationDeviance = 0.0; + if(cmd.containsOptionByName("--colorchecker")) { + MyColorChecker myColorChecker = new MyColorChecker(region); + + // fill ColorChecker + myColorChecker.fillColorChecker(); + + containers = detectProcessor.detectSeedContours(region, myColorChecker.getPixPerMM()); + + if(cmd.containsOptionByName("--calibrate")) { + Pair calibratedPair = myColorChecker.calibrateImg(); + region = calibratedPair.getLeft(); + transformationDeviance = calibratedPair.getRight(); + + //System.out.println("# Transformation deviance: " + transformationDeviance.toString()); + } + } else { + containers = detectProcessor.detectSeedContours(region); + } + + //System.out.println("Seed count: " + containers.size()); + + saveResults(region, containers, writer, inFile.getName(), year, sampleName, group, mixing, duplicate, transformationDeviance); + + + if(cmd.containsOptionByName("--draw")) { + drawContoursWithRectAndNum(region, containers); + Imgcodecs.imwrite(outFile.getCanonicalPath(), region); + } + + + // Releases + for(PlanarObject container : containers) { + container.getContour().release(); + } + containers.clear(); + + if(!srcImg.empty()) { + srcImg.release(); + } + + if(!region.empty()) { + region.release(); + } + + System.out.println(inFile.getParent() + "/" + inFile.getName() + " done."); + + } catch(IncorrectCmdException ex) { + service.shutdown(); + } catch (SeedCounterProcessorException e) { + e.printStackTrace(); + continue; + } catch (IOException e) { + e.printStackTrace(); + continue; + } + } + + service.shutdown(); + } + + + private static void drawContoursWithRectAndNum(Mat img, List containers) { + for(int i=0; i containers, PrintWriter writer, String fileName, String year, String sampleName, String group, String mixing, String duplicate, double transformationDeviance) throws IOException { + + for(int i=0; i cluster) { + Clusterable center = cluster.getCenter(); + List points = cluster.getPoints(); + + double maxDistance = 0; + for(ContourContainer point : points) { + double curDist = euclideanDistance(center.getPoint(), point.getPoint()); + + if(curDist > maxDistance) { + maxDistance = curDist; + } + } + + return maxDistance; + } + + + public static double euclideanDistance(double[] point1, double[] point2) { + int dimensions = point1.length < point2.length ? point1.length : point2.length; + + double quadSum = 0; + for(int i=0; i headers(); + + public String toString(String separator); + + public AddInfo from(File inFile); +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/BaseResultSaver.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/BaseResultSaver.java new file mode 100644 index 0000000..33055df --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/BaseResultSaver.java @@ -0,0 +1,62 @@ +package org.wheatdb.seedcounter.desktop.ihandlers; + +import java.io.PrintWriter; +import java.util.LinkedList; +import java.util.List; + +import org.opencv.core.Mat; +import org.wheatdb.seedcounter.processor.PlanarObject; + +public class BaseResultSaver implements ResultSaver { + private AddInfo info; + + @Override + public void saveResults(PrintWriter writer, Mat region, List containers, String fileName, double transformationDeviance) { + for(int i=0; i columnNames = new LinkedList(); + + columnNames.addAll(info.headers()); + + columnNames.add("D"); + columnNames.add("Seed_number"); + columnNames.add("Length"); + columnNames.add("Width"); + columnNames.add("Area"); + columnNames.add("Circularity"); + columnNames.add("Roundness"); + columnNames.add("Rugosity"); + columnNames.add("Solidity"); + //columnNames.addAll(SeedData.headers()); + + for(String columnName : columnNames) { + writer.print(columnName + SEPARATOR); + } + } + + + public void setInfo(AddInfo info) { + this.info = info; + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/ClassicImageHandler.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/ClassicImageHandler.java new file mode 100644 index 0000000..b8ae53f --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/ClassicImageHandler.java @@ -0,0 +1,137 @@ +package org.wheatdb.seedcounter.desktop.ihandlers; + +import static org.wheatdb.seedcounter.desktop.AppPrint.*; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; + +import org.opencv.core.Mat; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; +import org.wheatdb.imgproc.colorchecker.MyColorChecker; +import org.wheatdb.seedcounter.SeedCounterException; +import org.wheatdb.seedcounter.desktop.JSONSaver; +import org.wheatdb.seedcounter.desktop.RunConfiguration; +import org.wheatdb.seedcounter.processor.DetectionProcessor; +import org.wheatdb.seedcounter.processor.PlanarObject; +import org.wheatdb.seedcounter.processor.SeedCounterProcessorException; +import org.wheatdb.seedcounter.processor.subjects.Quad; + +import ru.delkom07.util.Pair; + +public class ClassicImageHandler implements ImageHandler { + private DetectionProcessor detectProcessor; // = new DetectionProcessor(SheetFormat.A4); // from DI container + private RunConfiguration config; // from DI container + private ResultSaver resultSaver; // from DI container + + + + public ClassicImageHandler(RunConfiguration config, DetectionProcessor detectProcessor, ResultSaver resultSaver) { + this.config = config; + this.detectProcessor = detectProcessor; + this.resultSaver = resultSaver; + } + + + public void handleImage(File inFile, File outFile, PrintWriter writer) throws IOException, SeedCounterProcessorException, SeedCounterException { + Mat srcImg = Imgcodecs.imread(inFile.getCanonicalPath(), Imgcodecs.CV_LOAD_IMAGE_UNCHANGED); + + if(srcImg.empty()) + throw new SeedCounterException("Input file is not image."); + + Mat region = srcImg; + if(!config.withoutsheet) { + Quad quad = detectProcessor.detectQuad(srcImg); + region = detectProcessor.getTransformedField(srcImg, quad); + } + + + List containers; + + // if colorchecker is on the image + Double transformationDeviance = 0.0; + if(config.colorchecker) { + MyColorChecker myColorChecker = new MyColorChecker(region); + + // fill ColorChecker + myColorChecker.fillColorChecker(); + + if(config.calibrate) { + Pair calibratedPair = myColorChecker.calibrateImg(); + + // resources need to be release + if(!region.empty()) { + region.release(); + } + + region = calibratedPair.getLeft(); + transformationDeviance = calibratedPair.getRight(); + + print("# Transformation deviance: " + transformationDeviance.toString()); + } + + containers = detectProcessor.detectSeedContours(region, myColorChecker.getPixPerMM()); + } else { + containers = detectProcessor.detectSeedContours(region.clone()); + } + + + print("Seed count: " + containers.size()); + + resultSaver.saveResults(writer, region, containers, inFile.getName(), transformationDeviance); + + new JSONSaver().saveInJSON(region, containers, inFile, outFile); + + if(config.draw) { + if(config.markdescriptor) { + ColorDescriptorsSet.drawContoursWithRectAndNumAndMarkGCH(region, containers, config.colorSpace, config.markGCHDimension, config.markGCHBinNumber); + } else { + drawContoursWithRectAndNum(region, containers); + } + + Imgcodecs.imwrite(outFile.getCanonicalPath(), region); + } + + + // Releases + for(PlanarObject container : containers) { + container.getContour().release(); + } + containers.clear(); + + if(!srcImg.empty()) { + srcImg.release(); + } + + if(!region.empty()) { + region.release(); + } + } + + + private void drawContoursWithRectAndNum(Mat img, List containers) { + for(int i=0; i containers, ColorSpace colorSpace, int gchDimension, int binNumber) { + for(int i=0; i containers, String fileName, double transformationDeviance) { + for(int i=0; i columnNames = new LinkedList(); + + // RGB + columnNames.add("RGB_meanColor_R"); + columnNames.add("RGB_meanColor_G"); + columnNames.add("RGB_meanColor_B"); + + for(int i=0; i<64; i++) { + columnNames.add("RGB_GCH4_" + (i+1)); + } + + for(int i=0; i<512; i++) { + columnNames.add("RGB_GCH8_" + (i+1)); + } + + for(int i=0; i<12; i++) { + columnNames.add("RGB_colorLayout_R_" + (i+1)); + columnNames.add("RGB_colorLayout_G_" + (i+1)); + columnNames.add("RGB_colorLayout_B_" + (i+1)); + } + + for(int i=0; i<3; i++) { + columnNames.add("RGB_dominantColor_R_" + (i+1)); + columnNames.add("RGB_dominantColor_G_" + (i+1)); + columnNames.add("RGB_dominantColor_B_" + (i+1)); + columnNames.add("RGB_dominantColor_percent_" + (i+1)); + columnNames.add("RGB_dominantColor_variance_" + (i+1)); + + } + + columnNames.add("RGB_dominantColor_spatialCoherency"); + + // HSV + columnNames.add("HSV_meanColor_H"); + columnNames.add("HSV_meanColor_S"); + columnNames.add("HSV_meanColor_V"); + + for(int i=0; i<64; i++) { + columnNames.add("HSV_GCH4_" + (i+1)); + } + + for(int i=0; i<512; i++) { + columnNames.add("HSV_GCH8_" + (i+1)); + } + + for(int i=0; i<12; i++) { + columnNames.add("HSV_colorLayout_H_" + (i+1)); + columnNames.add("HSV_colorLayout_S_" + (i+1)); + columnNames.add("HSV_colorLayout_V_" + (i+1)); + } + + for(int i=0; i<3; i++) { + columnNames.add("HSV_dominantColor_H_" + (i+1)); + columnNames.add("HSV_dominantColor_S_" + (i+1)); + columnNames.add("HSV_dominantColor_V_" + (i+1)); + columnNames.add("HSV_dominantColor_persent_" + (i+1)); + columnNames.add("HSV_dominantColor_variance_" + (i+1)); + } + + columnNames.add("HSV_dominantColor_spatialCoherency"); + + // L'a'b' + columnNames.add("Lab_meanColor_L"); + columnNames.add("Lab_meanColor_a"); + columnNames.add("Lab_meanColor_b"); + + for(int i=0; i<64; i++) { + columnNames.add("Lab_GCH4_" + (i+1)); + } + + for(int i=0; i<512; i++) { + columnNames.add("Lab_GCH8_" + (i+1)); + } + + for(int i=0; i<12; i++) { + columnNames.add("Lab_colorLayout_L_" + (i+1)); + columnNames.add("Lab_colorLayout_a_" + (i+1)); + columnNames.add("Lab_colorLayout_b_" + (i+1)); + } + + for(int i=0; i<3; i++) { + columnNames.add("Lab_dominantColor_L_" + (i+1)); + columnNames.add("Lab_dominantColor_a_" + (i+1)); + columnNames.add("Lab_dominantColor_b_" + (i+1)); + columnNames.add("Lab_dominantColor_persent_" + (i+1)); + columnNames.add("Lab_dominantColor_variance_" + (i+1)); + } + + columnNames.add("Lab_dominantColor_spatialCoherency"); + + // YCrCb + columnNames.add("YCrCb_meanColor_Y"); + columnNames.add("YCrCb_meanColor_Cr"); + columnNames.add("YCrCb_meanColor_Cb"); + + for(int i=0; i<64; i++) { + columnNames.add("YCrCb_GCH4_" + (i+1)); + } + + for(int i=0; i<512; i++) { + columnNames.add("YCrCb_GCH8_" + (i+1)); + } + + for(int i=0; i<12; i++) { + columnNames.add("YCrCb_colorLayout_Y_" + (i+1)); + columnNames.add("YCrCb_colorLayout_Cr_" + (i+1)); + columnNames.add("YCrCb_colorLayout_Cb_" + (i+1)); + } + + for(int i=0; i<3; i++) { + columnNames.add("YCrCb_dominantColor_Y_" + (i+1)); + columnNames.add("YCrCb_dominantColor_Cr_" + (i+1)); + columnNames.add("YCrCb_dominantColor_Cb_" + (i+1)); + columnNames.add("YCrCb_dominantColor_persent_" + (i+1)); + columnNames.add("YCrCb_dominantColor_variance_" + (i+1)); + } + + columnNames.add("YCrCb_dominantColor_spatialCoherency"); + + + for(String columnName : columnNames) { + writer.print(columnName + SEPARATOR); + } + } + + @Override + public void setInfo(AddInfo info) { + this.baseSaver.setInfo(info); + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/FileStructureAddInfo.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/FileStructureAddInfo.java new file mode 100644 index 0000000..543a0f6 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/FileStructureAddInfo.java @@ -0,0 +1,86 @@ +package org.wheatdb.seedcounter.desktop.ihandlers; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.io.FilenameUtils; + + +/** + * Дополнительная информация о входные данных, извлекаемая из файловой структуры, специальных файлов описания и т.п. + * Функционал класса планируется постоянно расширять. + * @author Komyshev + */ +public class FileStructureAddInfo implements AddInfo { + public String file; + public String year; + public String sampleName; + public String group; + public String mixing; + public String duplicate; + + public FileStructureAddInfo() {} + + private FileStructureAddInfo(String file, String year, String sampleName, String group, String mixing, String duplicate) { + this.file = file; + this.year = year; + this.sampleName = sampleName; + this.group = group; + this.mixing = mixing; + this.duplicate = duplicate; + } + + public FileStructureAddInfo from(File inFile) { + try { + String group = inFile.getParentFile().getName(); + String[] splitedSampleDirName = inFile.getParentFile().getParentFile().getName().split(" "); + String sampleName; + String year; + if(splitedSampleDirName.length == 3) { + sampleName = splitedSampleDirName[0] + "_" + splitedSampleDirName[1]; + year = splitedSampleDirName[2]; + } else + if(splitedSampleDirName.length == 2){ + sampleName = splitedSampleDirName[0] + "_" + splitedSampleDirName[1]; + year = "-"; + } else { + sampleName = splitedSampleDirName[0]; + year = "-"; + } + + String mixing = FilenameUtils.getBaseName(inFile.getName()).split("_")[0]; + String duplicate = FilenameUtils.getBaseName(inFile.getName()).split("_")[1]; + + return new FileStructureAddInfo(inFile.getName(), year, sampleName, group, mixing, duplicate); + } catch(Exception ex) { + return new FileStructureAddInfo(inFile.getName(), "-", "-", "-", "-", "-"); + } + } + + public List headers() { + var columnNames = new LinkedList(); + + columnNames.add("File"); + columnNames.add("Year"); + columnNames.add("Sample"); + columnNames.add("Group"); + columnNames.add("Mixing"); + columnNames.add("Duplicate"); + + return columnNames; + } + + public String toString(String separator) { + var builder = new StringBuilder(); + + builder.append(file + separator); + builder.append(year + separator); + builder.append(sampleName + separator); + builder.append(group + separator); + builder.append(mixing + separator); + builder.append(duplicate + separator); + + return builder.toString(); + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/ImageHandler.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/ImageHandler.java new file mode 100644 index 0000000..955252e --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/ImageHandler.java @@ -0,0 +1,16 @@ +package org.wheatdb.seedcounter.desktop.ihandlers; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +import org.wheatdb.seedcounter.SeedCounterException; +import org.wheatdb.seedcounter.processor.SeedCounterProcessorException; + +public interface ImageHandler { + public void handleImage(File inFile, File outFile, PrintWriter writer) throws IOException, SeedCounterProcessorException, SeedCounterException; + + public ResultSaver getResultSaver(); + + public void setResultSaver(ResultSaver resultSaver); +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/ResultSaver.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/ResultSaver.java new file mode 100644 index 0000000..1f5b4cd --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/ResultSaver.java @@ -0,0 +1,19 @@ +package org.wheatdb.seedcounter.desktop.ihandlers; + +import java.io.PrintWriter; +import java.util.List; + +import org.opencv.core.Mat; +import org.wheatdb.seedcounter.processor.PlanarObject; + +public interface ResultSaver { + public static final String SEPARATOR = " "; + + public void saveResults(PrintWriter writer, Mat region, List containers, String fileName, double transformationDeviance); + + public void writeData(PrintWriter writer, Mat region, PlanarObject container, int seedNumber, double transformationDeviance); + + public void writeFileHead(PrintWriter writer); + + public void setInfo(AddInfo info); +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/SimpleAddInfo.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/SimpleAddInfo.java new file mode 100644 index 0000000..1556a3f --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/SimpleAddInfo.java @@ -0,0 +1,36 @@ +package org.wheatdb.seedcounter.desktop.ihandlers; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; + +public class SimpleAddInfo implements AddInfo { + public String file; + + @Override + public List headers() { + var columnNames = new LinkedList(); + + columnNames.add("File"); + + return columnNames; + } + + @Override + public String toString(String separator) { + var builder = new StringBuilder(); + + builder.append(file + separator); + + return builder.toString(); + } + + @Override + public AddInfo from(File inFile) { + var info = new SimpleAddInfo(); + info.file = inFile.getName(); + + return info; + } + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/TextureDescriptorsSet.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/TextureDescriptorsSet.java new file mode 100644 index 0000000..c8e4a5e --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/TextureDescriptorsSet.java @@ -0,0 +1,64 @@ +package org.wheatdb.seedcounter.desktop.ihandlers; + +import java.util.Arrays; + +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; +import org.wheatdb.seedcounter.processor.ImageArea; + +import ru.delkom07.improc.texture.MaskedGLCM; +import ru.delkom07.improc.texture.MaskedGLRM; + +public class TextureDescriptorsSet { + MaskedGLCM maskedGLCM; + MaskedGLRM maskedGLRM; + + + TextureDescriptorsSet(Mat region, ImageArea imageArea) { + MatOfPoint contour = imageArea.getContour(); + Rect currentSeedRoi = Imgproc.boundingRect(contour); + Mat subMatBGR = region.submat(currentSeedRoi); + + Mat subMatGray = new Mat(); + Imgproc.cvtColor(subMatBGR, subMatGray, Imgproc.COLOR_BGR2GRAY); + + MatOfPoint shiftedContour = shiftContour(contour, -currentSeedRoi.x, -currentSeedRoi.y); + Mat seedMask = Mat.zeros(currentSeedRoi.height, currentSeedRoi.width, CvType.CV_8UC1); + Imgproc.drawContours(seedMask, Arrays.asList(shiftedContour), -1, new Scalar(255), -1); + + // Descriptors + // GLCM + int d = 3; + maskedGLCM = new MaskedGLCM(d).calculate(subMatGray, seedMask); + + // GLRM + int maxLength = 10; + maskedGLRM = new MaskedGLRM(maxLength).calculate(subMatGray, seedMask); + + subMatBGR.release(); + subMatGray.release(); + + seedMask.release(); + shiftedContour.release(); + } + + + private static MatOfPoint shiftContour(MatOfPoint contour, int shiftX, int shiftY) { + Point[] pointArr = new Point[contour.rows()*contour.cols()]; + + int i = 0; + for(Point srcPoint : contour.toArray()) { + pointArr[i++] = new Point(srcPoint.x+shiftX, srcPoint.y+shiftY); + } + + MatOfPoint shiftedContour = new MatOfPoint(); + shiftedContour.fromArray(pointArr); + return shiftedContour; + } +} + diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/TextureResultSaver.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/TextureResultSaver.java new file mode 100644 index 0000000..8a4c03a --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/desktop/ihandlers/TextureResultSaver.java @@ -0,0 +1,72 @@ +package org.wheatdb.seedcounter.desktop.ihandlers; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.opencv.core.Mat; +import org.wheatdb.seedcounter.processor.PlanarObject; + +import ru.delkom07.improc.texture.GLCM; +import ru.delkom07.improc.texture.GLRM; + +public class TextureResultSaver implements ResultSaver { + private TextureDescriptorsSet td; + + private ResultSaver baseSaver; + + public TextureResultSaver() {} + + public TextureResultSaver(ResultSaver baseSaver) { + this.baseSaver = baseSaver; + } + + + @Override + public void saveResults(PrintWriter writer, Mat region, List containers, String fileName, double transformationDeviance) { + for(int i=0; i columnNames = new LinkedList(); + + // GLCM + columnNames.addAll(Arrays.asList(GLCM.ChNames())); + + // GLRM + columnNames.addAll(Arrays.asList(GLRM.ChNames())); + + for(String columnName : columnNames) { + writer.print(columnName + SEPARATOR); + } + } + + @Override + public void setInfo(AddInfo info) { + this.baseSaver.setInfo(info); + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/BinaryMaskProcessor.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/BinaryMaskProcessor.java new file mode 100644 index 0000000..e9fbd11 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/BinaryMaskProcessor.java @@ -0,0 +1,137 @@ +package org.wheatdb.seedcounter.processor; + +import java.util.ArrayList; +import java.util.List; + +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.RotatedRect; +import org.opencv.imgproc.Imgproc; +import org.wheatdb.seedcounter.SeedCounterException; +import org.wheatdb.seedcounter.SeedDataContainer; + +import ru.delkom07.geometry.AdditionalIndexies; +import ru.delkom07.geometry.ContoursFunctions; +import ru.delkom07.geometry.SimpleGeometry; +import ru.delkom07.geometry.obj.Circle; +import ru.delkom07.geometry.obj.Vector; + + +/** + * Процессор для поиска контуров зерен по бинарной маске. + * Необходим рефакторинг, т.к. частично повторяет DetectionProcessor. + * @author Komyshev + */ +public class BinaryMaskProcessor implements Processor { + + public List detectSeedContours(Mat mask, double pixPerMM) throws SeedCounterProcessorException { + List contours = new ArrayList(); + + // Find contours + Imgproc.findContours(mask, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); + + // Gauge seeds + List seedDataList = new ArrayList(contours.size()); + for(int i=0; i 5) { // There should be at least 5 points to fit the ellipse + SeedDataContainer seedDataContainer = gaugeSeed(contour, xScale, yScale); + + seedDataList.add(seedDataContainer); + } + } + + return seedDataList; + } + + + /** + * Get info about seed on image (size in millimeters). + * @param image - source image that containing seed. + * @return information about seeds. + * @throws SeedCounterException - if DetectionProcessor isn't initialized. + */ + private SeedDataContainer gaugeSeed(MatOfPoint contour, double xScale, double yScale) throws SeedCounterProcessorException { + // get contour ellipse approximation + MatOfPoint2f forFillEllipse = new MatOfPoint2f(); + contour.convertTo(forFillEllipse, CvType.CV_32FC2); + RotatedRect rotatedRect = Imgproc.fitEllipse(forFillEllipse); + Point[] points = new Point[4]; + rotatedRect.points(points); + + // determine seed orientation related to sheet + Vector seedVector = new Vector(points[0], points[1]); + double dx = Math.abs(seedVector.x()); + double dy = Math.abs(seedVector.y()); + + double lamdaX = dx/(dx+dy); + double lamdaY = dy/(dx+dy); + + double lengthDistortionFactor = lamdaX*xScale + lamdaY*yScale; + double widthDistortionFactor = lamdaX*yScale + lamdaY*xScale; + + double seedLength = SimpleGeometry.distance(points[0], points[1])*lengthDistortionFactor; + double seedWidth = SimpleGeometry.distance(points[1], points[2])*widthDistortionFactor; + + // area + double areaInPix = Imgproc.contourArea(contour, false); + double area = areaInPix * lengthDistortionFactor*widthDistortionFactor; + + // calculate mass center + Rect seedRect = Imgproc.boundingRect(contour); + double[] globalMassCenter = ContoursFunctions.massCenter(contour); + double[] localMassCenter = new double[]{globalMassCenter[0]-seedRect.x, globalMassCenter[1]-seedRect.y}; + + // axis points + double[] lengthAxisPoint1 = new double[]{(points[0].x+points[1].x)/2.0, (points[0].y+points[1].y)/2.0}; + double[] lengthAxisPoint2 = new double[]{(points[2].x+points[3].x)/2.0, (points[2].y+points[3].y)/2.0}; + double[] widthAxisPoint1 = new double[]{(points[1].x+points[2].x)/2.0, (points[1].y+points[2].y)/2.0}; + double[] widthAxisPoint2 = new double[]{(points[0].x+points[3].x)/2.0, (points[0].y+points[3].y)/2.0}; + + if(SimpleGeometry.distance(lengthAxisPoint1, lengthAxisPoint2)maxChannels.val[0]) { + maxChannels.val[0] = color.val[0]; + } + if(color.val[1]>maxChannels.val[1]) { + maxChannels.val[1] = color.val[1]; + } + if(color.val[2]>maxChannels.val[2]) { + maxChannels.val[2] = color.val[2]; + } + + if(color.val[0] g_ ? r_ : g_) > b_ ? (r_ > g_ ? r_ : g_) : b_; + double cMin = (r_ > g_ ? g_ : r_) > b_ ? b_ : (r_ > g_ ? g_ : r_); + double delta = cMax - cMin; + + double hh = 0; + if(cMax==r_) { + double k = ((g_-b_)/delta); + hh = (k-( ((int)(k/6))*6))*60; + } else + if(cMax==g_) { + hh = (((b_-r_)/delta)+2)*60; + } else { + hh = (((r_-g_)/delta)+4)*60; + } + + double h = inrange256 ? Math.round(hh/360f*255f) : hh; + double s = inrange256 ? Math.round(delta/cMax*255f) : delta/cMax; + double v = inrange256 ? Math.round(cMax*255f) : cMax; + + return new Scalar(h, s, v); + } +} \ No newline at end of file diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/DetectionProcessor.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/DetectionProcessor.java new file mode 100644 index 0000000..e63157e --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/DetectionProcessor.java @@ -0,0 +1,607 @@ +package org.wheatdb.seedcounter.processor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.RotatedRect; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; +import org.wheatdb.seedcounter.SeedCounterException; +import org.wheatdb.seedcounter.SeedDataContainer; +import org.wheatdb.seedcounter.SheetFormat; +import org.wheatdb.seedcounter.processor.ex.QuadNotFoundException; +import org.wheatdb.seedcounter.processor.filters.GrayBinarization; +import org.wheatdb.seedcounter.processor.filters.HSVBinarization; +import org.wheatdb.seedcounter.processor.filters.SeedsBinarizationForWhiteSeeds; +import org.wheatdb.seedcounter.processor.subjects.Quad; + +import ru.delkom07.geometry.AdditionalIndexies; +import ru.delkom07.geometry.ContoursFunctions; +import ru.delkom07.geometry.SimpleGeometry; +import ru.delkom07.geometry.obj.Circle; +import ru.delkom07.geometry.obj.Vector; +import ru.delkom07.util.Pair; + + +/** + * SeedCountProcessor produces detection and measurement of seed. + * @version 2.0 (20.12.23) + */ +public class DetectionProcessor implements Processor { + protected double MIN_SIDE_OF_QUAD = 0.33; // ~ minimal side size of quad relation by image height + + // in millimeters + protected double mmInBigSideOfSheet = 297; // A4 for default + protected double mmInSmallSideOfSheet = 210; // A4 for default + + double minSeedLength = 4; // !!!!!!!!!!!!!!!! SeedCounterApplication.config.getMinSeedLength() + double maxSeedLength = 14; // !!!!!!!!!!!!!!!! SeedCounterApplication.config.getMaxSeedLength() + + + // paddings for sheet transformation + protected int widthPadding = -1; + protected int heightPadding = -1; + + public DetectionProcessor() { + setSheetFormat(SheetFormat.A4); + } + + + public DetectionProcessor(SheetFormat sheetFormat) { + setSheetFormat(sheetFormat); + } + + public void setSheetFormat(SheetFormat sheetFormat) { + mmInBigSideOfSheet = sheetFormat.getBigSideSize(); + mmInSmallSideOfSheet = sheetFormat.getSmallSideSize(); + } + + + // -- Detection -- + /** + * Detection of white sheet. + * @param image - source image. + * @return detected quad of white sheet. + * @throws SeedCounterProcessorException + */ + public Quad detectQuad(Mat image) throws QuadNotFoundException { +// int width = image.cols(); +// int height = image.rows(); + + Mat gray = new Mat(image.rows(), image.cols(), CvType.CV_8UC1); + Imgproc.cvtColor(image, gray, Imgproc.COLOR_RGBA2GRAY); + Mat hulled = new Mat(image.rows(), image.cols(), CvType.CV_8UC1); + Imgproc.threshold(gray, hulled, 120, 255, Imgproc.THRESH_BINARY ); /// 185!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + gray.release(); + + List contours = new LinkedList(); + Imgproc.findContours(hulled.clone(), contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE); + + MatOfPoint contour = ContoursFunctions.getMaxAreaContour(contours); + + MatOfPoint2f contour2f = new MatOfPoint2f(contour.toArray()); + double perimeter = Imgproc.arcLength(contour2f, true); + + MatOfPoint2f approx2f = new MatOfPoint2f(); + Imgproc.approxPolyDP(contour2f, approx2f, 0.02 * perimeter, true); + + if(approx2f.size().height*approx2f.size().width==4) { + return new Quad(approx2f.toArray()); + } else { + throw new QuadNotFoundException(String.format("DetectionProcessor: the approximation has %s vertices.", approx2f.size().height*approx2f.size().width)); + } + } + + + + /** + * Detect contours of seeds in image. + * @param image - source image. + * @return list of contours. + */ + public List detectSeedContours(Mat image, double pixPerMM) throws SeedCounterProcessorException { + List contours = new ArrayList(); + + HSVBinarization seedContourBinarization = new SeedsBinarizationForWhiteSeeds(); + Mat hulled = seedContourBinarization.apply(image); + + // Find contours + Imgproc.findContours(hulled, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); + hulled.release(); + + // Gauge seeds + List seedDataList = new ArrayList(contours.size()); + for(int i=0; i 5) { // There should be at least 5 points to fit the ellipse + SeedDataContainer seedDataContainer = gaugeSeed(contour, xScale, yScale); + + double seedLen = seedDataContainer.getLenght(); + if(minSeedLength detectSeedContours(Mat image) throws SeedCounterProcessorException { + // init size + //prepareSize(image.cols(), image.rows()); + if(-1 == widthPadding || -1 == heightPadding) + throw new SeedCounterProcessorException("Padding isn't initialized."); + + + int width = image.cols(); + int height = image.rows(); + + // Clean + GrayBinarization binarization = new GrayBinarization(true, width*height/(210*297)); // ????????????????????????????? + Mat hulled = binarization.apply(image); + + // Find contours + List contours = new ArrayList(); + Imgproc.findContours(hulled, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); + + /* // disabled for desktop + if(SeedCounterApplication.config.getUseWatershed()) { + double maxSeedAreaInMM2 = 78; + double maxSeedAreaInPix = maxSeedAreaInMM2 * ((width+2*widthPadding)/mmInBigSideOfSheet); + contours = clarifyContours(contours, maxSeedAreaInPix); + } + */ + + double pixInMM = (width+2*widthPadding)/mmInBigSideOfSheet; + + // Gauge seeds + List seedDataList = new ArrayList(contours.size()); + for(int i=0; i 5) { // There should be at least 5 points to fit the ellipse + SeedDataContainer seedDataContainer = gaugeSeed(contour, xScale, yScale); + + double seedLen = seedDataContainer.getLenght(); + if(minSeedLength clarifyContours(List contours, double maxContourArea) { + List clarifyedContours = new LinkedList(); + + for(MatOfPoint contour : contours) { + boolean isGlue = isGlueContours(contour); + + double contourArea = Imgproc.contourArea(contour); + + if(isGlue || contourArea > maxContourArea) { + Rect rect = Imgproc.boundingRect(contour); + Rect roi = new Rect(rect.x, rect.y, rect.width, rect.height); + + clarifyedContours.addAll(clarifyContour(contour, roi)); + } else { + clarifyedContours.add(contour); + } + } + + return clarifyedContours; + } + + private List clarifyContour(MatOfPoint contour, Rect roi) { + MatOfPoint shiftedContour = shiftContour(contour, -roi.x, -roi.y); + + Mat imprint = Mat.zeros(roi.size(), CvType.CV_8UC3); + Imgproc.drawContours(imprint, Arrays.asList(shiftedContour), -1, new Scalar(255, 255, 255), -1); + + List separatedAndShiftedContours = watershedContours(imprint); + List separatedContours = new ArrayList(); + + if(null == separatedAndShiftedContours) { + separatedContours.add(contour); + return separatedContours; + } + + for(MatOfPoint separatedAndShiftedContour : separatedAndShiftedContours) { + separatedContours.add(shiftContour(separatedAndShiftedContour, roi.x, roi.y)); + } + + return separatedContours; + } + + + private MatOfPoint shiftContour(MatOfPoint contour, int shiftX, int shiftY) { + Point[] pointArr = new Point[contour.rows()*contour.cols()]; + + int i = 0; + for(Point srcPoint : contour.toArray()) { + pointArr[i++] = new Point(srcPoint.x+shiftX, srcPoint.y+shiftY); + } + + MatOfPoint shiftedContour = new MatOfPoint(); + shiftedContour.fromArray(pointArr); + return shiftedContour; + } + + + private static List watershedContours(Mat imprint) { + Mat binarized = new Mat(); + Imgproc.cvtColor(imprint, binarized, Imgproc.COLOR_RGBA2GRAY); + + // noise removal + Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3)); + Mat opening = new Mat(); + Imgproc.morphologyEx(binarized, opening, Imgproc.MORPH_OPEN, kernel); + + // sure background area + Mat sureBG = new Mat(); + Imgproc.dilate(opening, sureBG, kernel); + + // finding sure foreground area + Mat distTransform = new Mat(); + Imgproc.distanceTransform(opening, distTransform, Imgproc.CV_DIST_L2, 5); + Mat sureFG = new Mat(); + //double epsilon = 0.55+((SeedCounterApplication.config.getWatershedSensivity()/100.0)*0.7)*(Core.minMaxLoc(distTransform)).maxVal; //from 0.5 to 0.8 // optimal = 0.65 + double epsilon = 0.55+((71/100.0)*0.7)*(Core.minMaxLoc(distTransform)).maxVal; //from 0.5 to 0.8 // optimal = 0.65 + + sureFG.convertTo(sureFG, CvType.CV_8UC1); + Mat unknown = new Mat(); + Core.subtract(sureBG, sureFG, unknown); + + List contours = new ArrayList(); + Imgproc.findContours(sureFG.clone(), contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); + + if(contours.size() < 2) { + return null; + } + + Mat markers = Mat.zeros(binarized.size(), CvType.CV_32SC1); + markers.setTo(new Scalar(1)); + for(int i=0; i separateMarkers(Mat markers, int markersNumber) { + List separatedContours = new ArrayList(); + + for(int k=0; k contour = new ArrayList(); + Imgproc.findContours(imprint, contour, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); + separatedContours.addAll(contour); + + imprint.release(); + } + + return separatedContours; + } + + + private static boolean isGlueContours(MatOfPoint contour) { + boolean isGlueContours = false; + + MatOfPoint2f contour2f = new MatOfPoint2f( contour.toArray() ); + + double contourLength = Imgproc.arcLength(contour2f, true); + double approxRadius = contourLength/(2*Math.PI); + double[] massCenter = ContoursFunctions.massCenter(contour); + + //int vectLength = (int)contourLength/64 > 4 ? (int)contourLength/64 : 4; + int vectLength = 8; + List vectors = ContoursFunctions.getVectors(contour, vectLength); + List> bendListIndxs = ContoursFunctions.getBendsIndxs(vectors); + + { + short toCenterVectorsCount = 0; + if(bendListIndxs.size() >= 2) { + for(Pair bend : bendListIndxs) { + Vector vector = bend.getRight(); + + double val = SimpleGeometry.fromLineToPointDistance(vector, massCenter); + if(val < approxRadius/2.0) { + toCenterVectorsCount++; + } + } + } + + if(toCenterVectorsCount >= 3) { + isGlueContours = true; + } + } + + if(bendListIndxs.size()>=2) { + for(int i=0; i 140) { + if(SimpleGeometry.fromLineToPointDistance(v1, v2.p1()) < approxRadius/4.0) { + isGlueContours = true; + } + + if(SimpleGeometry.fromLineToPointDistance(v2, v1.p1()) < approxRadius/4.0) { + isGlueContours = true; + } + } + + } + } + } + + return isGlueContours; + } + + + /** + * Sort contours by place on sheet in text order. + * @param contours + * @return + */ + public static List sortSeedContours(List contours, final int stringHeight) { + List sorted = new ArrayList(); + int counts = contours.size(); + + while(counts != sorted.size()) { + MatOfPoint next = Collections.min(contours, new Comparator() { + @Override + public int compare(MatOfPoint o1, MatOfPoint o2) { + double[] p1 = o1.get(0, 0); + double[] p2 = o2.get(0, 0); + + if(p1[1]stringHeight) { + return -1; + } + + if(p1[0] getSeedsRotatedRects(List contours) { + List quads = new ArrayList(); + for(int i=0; i contours) { + Imgproc.drawContours(img, contours, -1, seedColor); + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/ElementsProcessor.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/ElementsProcessor.java new file mode 100644 index 0000000..5021a8c --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/ElementsProcessor.java @@ -0,0 +1,183 @@ +package org.wheatdb.seedcounter.processor; + +/** + * DELETE IT! + * Processor for elementary operations with points, lines and etc. + * @author Komyshev + * v.1.0. (20.12.23) + */ +public class ElementsProcessor { + /** + * Is targeting point near to middle normal of vector defined by two points. + * @param p1 - first point. + * @param p2 - second point. + * @param p - targeting point. + * @param sizeOfRegion - threshold of close. + * @return true if targeting point lies on middle normal of given points and false otherwise. + * @deprecated !!! Error: function calculates incorrect distance from point to line: |f(Point.x) - Point.y| is not correct distance. + */ +// @Deprecated +// private static boolean isOnMiddleNormal(Point p1, Point p2, Point p, double sizeOfRegion) { +// // Middle point +// double xMdl = (p1.x+p2.x)/2.0; +// double yMdl = (p1.y+p2.y)/2.0; +// +// // y(x) = k*x + b; +// double k = (double)(p2.x-p1.x)/(double)(p2.y-p1.y); +// double b = yMdl + k*(double)xMdl; +// double y = k*(double)p.x + b; //?? +// +// // y - value of function in p.x - should be near to p.y +// if(Math.abs(p.y - y) filterCircularContours(List contours, double minSeedLength, double maxSeedLength) { +// List filtered = new ArrayList(); +// +// for(MatOfPoint contour : contours) { +// // Filter big length object +// if(containsTwoDistantPoints(contour, maxSeedLength)) +// continue; +// +// // Filter small length object +// if(!containsTwoDistantPoints(contour, minSeedLength)) +// continue; +// +// filtered.add(contour); +// } +// +// return filtered; +// } + + + /** + * Calculate whether mat contains two point with distance over threshold. + * @param mat - matrix of points. + * @param thres - threshold of distance. + * @return true if contains, and false otherwise. + */ +// private static boolean containsTwoDistantPoints(MatOfPoint mat, double thres) { +// for(int i=0; i thres) return true; +// } +// } +// } +// } +// +// return false; +// } + + + /** + * Find two points which have max distance between them. + * @param points - mat of points for search. + * @return two points which max distance between them. + */ +// private static double[][] findMaxDistancePoints(MatOfPoint points) { +// double maxDistance = 0; +// double[][] maxPoints = new double[2][2]; +// +// for(int i=0; imaxDistance) { +// maxDistance = distance; +// maxPoints[0] = p1; +// maxPoints[1] = p2; +// } +// } +// } +// return maxPoints; +// } + + + /** + * Calculate whether list contains two point with distance over threshold. + * @param list - list of points. + * @param thres - threshold of distance. + * @return true if contains, and false otherwise. + */ +// private static boolean containsTwoDistantPoints(List list, double thres) { +// for(int i=0; ithres) return true; +// } +// } +// return false; +// } + + + /** + * Calculate whether two mats contains two near point. + * @param mat1 - first mat of point. + * @param mat2 - second mat of point. + * @param thres - threshold of distance. + * @return true if contains two near point, and false otherwise. + */ +// private static boolean containsNearPoints(MatOfPoint mat1, MatOfPoint mat2, double thres) { +// // for each point in mat1 +// for(int i=0; i~ 18106549px (to 27159823) 232.486387686 dpmm + blockSize = withSeeds ? 201 : 33; + C = withSeeds ? 9 : 9; + medianBlur = withSeeds ? 0 : 5; + maxSpecklesSize = withSeeds ? 340 : 3500; + } else { // >~ 72419094 px 930.111992945 dpmm + blockSize = withSeeds ? 3551 : 71; // 251 + C = withSeeds ? 41 : 27 ; // 15 + medianBlur = withSeeds ? 7 : 7; + maxSpecklesSize = withSeeds ? 2500 : 5000; + } + + // 1551 + // 39 + + //medianBlur = medianBlur%2 == 0 ? medianBlur-1 : medianBlur; // should be odd + //blockSize = blockSize%2 == 0 ? blockSize+1 : blockSize; // should be odd + } + + @Override + public Mat apply(Mat img) { + int height = img.height(); + int width = img.width(); + + + Mat blured; + if(0!=medianBlur) { + blured = new Mat(img.rows(), img.cols(), CvType.CV_8UC3); + Imgproc.medianBlur(img, blured, (int)medianBlur); + img.release(); + } else { + blured = img; + } + + // to gray and threshold + Mat gray = new Mat(height, width, CvType.CV_8UC1); + Imgproc.cvtColor(blured, gray, Imgproc.COLOR_RGBA2GRAY); + blured.release(); + + Mat outImg = new Mat(height, width, CvType.CV_8UC1); + Imgproc.adaptiveThreshold(gray, outImg, 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, + inv?Imgproc.THRESH_BINARY_INV:Imgproc.THRESH_BINARY, blockSize, C); + gray.release(); + + + try { + Calib3d.filterSpeckles(outImg, inv? 0:255, maxSpecklesSize, maxDiff); + } catch(Exception e) { + int blur = (maxSpecklesSize/150); + Imgproc.medianBlur(outImg, outImg, blur%2==0 ? blur+1 : blur); + } + + + //String text = "\nblockSize="+blockSize+"\nC="+C+"\nmedianBlur="+medianBlur+"\nmaxSpecklesSize=" + maxSpecklesSize; + //System.out.println(text); + //Core.putText(outImg, text, new Point(outImg.cols()/10, outImg.rows()/10), 2, 0.3, new Scalar(0)); + + return outImg; + } + + /** + * Set invertion for binaryzation. + * @param inv - true for invertion and false otherwise. + */ + public void setInverting(boolean inv) { + this.inv = inv; + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/HSVBinarization.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/HSVBinarization.java new file mode 100644 index 0000000..ac493d9 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/HSVBinarization.java @@ -0,0 +1,86 @@ +package org.wheatdb.seedcounter.processor.filters; + +import java.util.List; + +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; + +import ru.delkom07.util.Pair; + + + +public class HSVBinarization { + private final List> targetsAndRanges; + + public HSVBinarization(List> targetsAndRanges) { + this.targetsAndRanges = targetsAndRanges; + } + + + public Mat apply(Mat input) { + Mat hsvImg = new Mat(input.size(), CvType.CV_8UC3); + Imgproc.cvtColor(input, hsvImg, Imgproc.COLOR_BGR2HSV); + + Mat output = Mat.zeros(input.size(), CvType.CV_8UC1); + for(int i=0; i targetAndRange : targetsAndRanges) { + Scalar target = targetAndRange.getLeft(); + Scalar range = targetAndRange.getRight(); + + if(Math.abs(value[0]-target.val[0]) targetAndRange : targetsAndRanges) { + Scalar target = targetAndRange.getLeft(); + Scalar range = targetAndRange.getRight(); + + if(Math.abs(value[0]-target.val[0])> targetsAndRanges = new LinkedList>(); + + static { + Scalar target1 = new Scalar(12, 153, 76.5); + Scalar range1 = new Scalar(30, 80, 120); + targetsAndRanges.add(new Pair(target1, range1)); + + Scalar target2 = new Scalar(19.125, 147.9, 153); + Scalar range2 = new Scalar(30, 80, 120); + targetsAndRanges.add(new Pair(target2, range2)); + + Scalar target3 = new Scalar(22.7, 119.85, 193.8); + Scalar range3 = new Scalar(30, 30, 100); + targetsAndRanges.add(new Pair(target3, range3)); + + Scalar target4 = new Scalar(32.6, 71.4, 204); + Scalar range4 = new Scalar(30, 30, 40); + targetsAndRanges.add(new Pair(target4, range4)); + + Scalar target5 = new Scalar(20, 66, 71); + Scalar range5 = new Scalar(20, 20, 10); + targetsAndRanges.add(new Pair(target5, range5)); + } + + + public SeedsBinarization() { + super(targetsAndRanges); + } + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/SeedsBinarization2.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/SeedsBinarization2.java new file mode 100644 index 0000000..fe21247 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/SeedsBinarization2.java @@ -0,0 +1,41 @@ +package org.wheatdb.seedcounter.processor.filters; + +import java.util.LinkedList; +import java.util.List; + +import org.opencv.core.Scalar; + +import ru.delkom07.util.Pair; + + +public class SeedsBinarization2 extends HSVBinarization { + static List> targetsAndRanges = new LinkedList>(); + + static { + Scalar target1 = new Scalar(161.0, 81.0, 144.0); + Scalar range1 = new Scalar(29.0, 43.0, 108.0); + targetsAndRanges.add(new Pair(target1, range1)); + + Scalar target2 = new Scalar(161.0, 72.0, 142.0); + Scalar range2 = new Scalar(27.0, 38.0, 109.0); + targetsAndRanges.add(new Pair(target2, range2)); + + Scalar target3 = new Scalar(15.0, 138.0, 207.0); + Scalar range3 = new Scalar(41.0, 94.0, 144.0); + targetsAndRanges.add(new Pair(target3, range3)); + + Scalar target4 = new Scalar(161.0, 72.0, 142.0); + Scalar range4 = new Scalar(27.0, 38.0, 109.0); + targetsAndRanges.add(new Pair(target4, range4)); + + Scalar target5 = new Scalar(37.0, 231.0, 41.0); + Scalar range5 = new Scalar(51.0, 198.0, 145.0); + targetsAndRanges.add(new Pair(target5, range5)); + } + + + public SeedsBinarization2() { + super(targetsAndRanges); + } + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/SeedsBinarizationForWhiteSeeds.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/SeedsBinarizationForWhiteSeeds.java new file mode 100644 index 0000000..57593d4 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/SeedsBinarizationForWhiteSeeds.java @@ -0,0 +1,29 @@ +package org.wheatdb.seedcounter.processor.filters; + +import java.util.LinkedList; +import java.util.List; + +import org.opencv.core.Scalar; + +import ru.delkom07.util.Pair; + + +public class SeedsBinarizationForWhiteSeeds extends HSVBinarization { + static List> targetsAndRanges = new LinkedList>(); + + static { + Scalar target1 = new Scalar(19.125, 147.9, 153); + Scalar range1 = new Scalar(30, 120, 120); + targetsAndRanges.add(new Pair(target1, range1)); + + Scalar target2 = new Scalar(32.6, 71.4, 204); + Scalar range2 = new Scalar(30, 30, 40); + targetsAndRanges.add(new Pair(target2, range2)); + } + + + public SeedsBinarizationForWhiteSeeds() { + super(targetsAndRanges); + } + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/WhiteBalanser.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/WhiteBalanser.java new file mode 100644 index 0000000..474d8d6 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/filters/WhiteBalanser.java @@ -0,0 +1,172 @@ +package org.wheatdb.seedcounter.processor.filters; + +import java.util.Arrays; + +import org.opencv.core.Mat; + +public class WhiteBalanser { + + public static void whiteBalance(Mat img) { + double[] rowR = new double[img.cols()*img.rows()]; + double[] rowG = new double[img.cols()*img.rows()]; + double[] rowB = new double[img.cols()*img.rows()]; + + for(int i=0; i 255 ? 255 : rValueBalanced); + rowG[l] = gValueBalanced < 0 ? 0 : (gValueBalanced > 255 ? 255 : gValueBalanced); + rowB[l] = bValueBalanced < 0 ? 0 : (bValueBalanced > 255 ? 255 : bValueBalanced); + } + + + for(int i=0; i < img.rows(); i++) { + for(int j=0; j < img.cols(); j++) { + img.put(i, j, new double[] {rowR[i*img.cols()+j], rowG[i*img.cols()+j], rowB[i*img.cols()+j]}); + } + } + + } + + + static double percentile(double[] array, double percentile) { + int nArray = array.length; + + double nPercent = (nArray + 1) * percentile / 100.0; + + if(nPercent == 1) { + return array[0]; + } else if(nPercent == nArray) { + return array[nArray - 1]; + } else { + int intNPercent = (int)nPercent; + double d = nPercent - intNPercent; + return array[intNPercent - 1] + d * (array[intNPercent] - array[intNPercent - 1]); + } + } + +} + + + + +/* + +package org.wheatdb.seedcounter.processor.filters; + +import java.util.Arrays; + +import org.opencv.core.Mat; + +public class WhiteBalanser { + + public static void whiteBalance(Mat img) { + double[] rowR = new double[img.cols()*img.rows()]; + double[] rowG = new double[img.cols()]; + double[] rowB = new double[img.cols()]; + + for(int i=0; i 255 ? 255 : rowR[j]); + rowG[j] = gValueBalanced < 0 ? 0 : (gValueBalanced > 255 ? 255 : rowG[j]); + rowB[j] = bValueBalanced < 0 ? 0 : (bValueBalanced > 255 ? 255 : rowB[j]); + } + + for(int j=0; j < img.cols(); j++) { + img.put(i, j, new double[] {rowR[j], rowG[j], rowB[j]}); + } + + } + } + + + static double percentile(double[] array, double percentile) { + int nArray = array.length; + + double nPercent = (nArray + 1) * percentile / 100.0; + + if(nPercent == 1) { + return array[0]; + } else if(nPercent == nArray) { + return array[nArray - 1]; + } else { + int intNPercent = (int)nPercent; + double d = nPercent - intNPercent; + return array[intNPercent - 1] + d * (array[intNPercent] - array[intNPercent - 1]); + } + } + +} + + + +*/ diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/subjects/Quad.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/subjects/Quad.java new file mode 100644 index 0000000..7dc3df2 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/subjects/Quad.java @@ -0,0 +1,175 @@ +package org.wheatdb.seedcounter.processor.subjects; + +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.imgproc.Imgproc; +import org.wheatdb.seedcounter.processor.SeedCounterProcessorException; +import org.wheatdb.seedcounter.processor.ex.QuadNotFoundException; + +/** + * Quad. + * @author Komyshev + * @version 0.2. + */ +public class Quad { + private Point tl = null; + private Point tr = null; + private Point br = null; + private Point bl = null; + + public Quad(Point[] points) throws QuadNotFoundException { + double cx = (points[0].x + points[1].x + points[2].x + points[3].x)/4.0; + double cy = (points[0].y + points[1].y + points[2].y + points[3].y)/4.0; + + definePoint(points[0], cx, cy); + definePoint(points[1], cx, cy); + definePoint(points[2], cx, cy); + definePoint(points[3], cx, cy); + + if(tl == tr || tr == br || br == bl || bl == tl || tl == br || tr == bl || tl == bl) + throw new QuadNotFoundException("Quad: Weird rectangle"); + } + + private void definePoint(Point p, double cx, double cy) { + if(p.x Math.abs(tl.y-bl.y)) { + return tl.x-tr.x; + } else { + return tl.y-bl.y; + } + } + + public double getSmallSideSize() { + if(Math.abs(tl.x-tr.x) > Math.abs(tl.y-bl.y)) { + return tl.y-bl.y; + } else { + return tl.x-tr.x; + } + } + + public Point tl() { + return tl; + } + + public Point tr() { + return tr; + } + + public Point bl() { + return bl; + } + + public Point br() { + return br; + } + + public boolean isInside(Point point) { + double minX = Math.min(Math.min(tl.x, tr.x), Math.min(bl.x, br.x)); + double minY = Math.min(Math.min(tl.y, tr.y), Math.min(bl.y, br.y)); + double maxX = Math.max(Math.max(tl.x, tr.x), Math.max(bl.x, br.x)); + double maxY = Math.max(Math.max(tl.y, tr.y), Math.max(bl.y, br.y)); + return point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY; + } + + public double getArea() { + double dx1 = tr.x - tl.x; + double dy1 = tr.y - tl.y; + double dx2 = bl.x - tl.x; + double dy2 = bl.y - tl.y; + + return Math.abs(dx1 * dy2 - dy1 * dx2); + } + + /** + * Recalculate relational points coordinates at source image for distinct image. The result is quad that has corresponding coordinates in distinct image. + * @param from is the source image. + * @param to is the distinct image. + * @param srcQuad is the quad for recalculate. + * @return recalculated quad. + */ + public static Quad recalculateQuadPoints(Mat from, Mat to, Quad srcQuad) { + int width = from.cols(); + int height = from.rows(); + + int newWidth = to.cols(); + int newHeight = to.rows(); + + Point newTl = new Point(srcQuad.tl().x/width*newWidth, srcQuad.tl().y/height*newHeight); + Point newTr = new Point(srcQuad.tr().x/width*newWidth, srcQuad.tr().y/height*newHeight); + Point newBl = new Point(srcQuad.bl().x/width*newWidth, srcQuad.bl().y/height*newHeight); + Point newBr = new Point(srcQuad.br().x/width*newWidth, srcQuad.br().y/height*newHeight); + + return new Quad(newTl, newTr, newBr, newBl); + } + + public Mat getTransformedField(Mat image) { + // Define the destination image + Mat transformed = new Mat(image.rows(), image.cols(), image.type()); + + // Corners of the destination image + Point[] quad_pts = new Point[4]; + quad_pts[0] = new Point(0, 0); + quad_pts[1] = new Point(transformed.cols(), 0); + quad_pts[2] = new Point(transformed.cols(), transformed.rows()); + quad_pts[3] = new Point(0, transformed.rows()); + + // Get transformation matrix + Mat transmtx = Imgproc.getPerspectiveTransform(new MatOfPoint2f(getPoints()), + new MatOfPoint2f(quad_pts)); + + // Apply perspective transformation + Imgproc.warpPerspective(image, transformed, transmtx, transformed.size()); + + return transformed; + } +} \ No newline at end of file diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/SeedSizesFromMicroscope.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/SeedSizesFromMicroscope.java new file mode 100644 index 0000000..a888789 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/SeedSizesFromMicroscope.java @@ -0,0 +1,282 @@ +package org.wheatdb.seedcounter.processor.test; + +public class SeedSizesFromMicroscope { + public static double[] arrData = { + 6.768271966, 3.977383901, // 1 + 7.194846395, 3.907059088, + 7.152581919, 4.270789414, + 7.18656747, 3.855635604, + 6.907621932, 4.414672834, + 7.116211957, 3.877207884, + 6.572688757, 3.118555435, + 7.295257783, 3.365848667, + 7.439182136, 2.9111525, + 7.072391985, 4.073834913, + 7.068605579, 2.974672015, + 7.084068442, 3.103235842, + 7.366084038, 4.339364293, + 7.222691828, 3.798747416, + 6.932601977, 3.138551751, + 6.742821179, 2.959669662, + 6.880073272, 3.636893919, + 7.042264475, 3.051198346, + 7.108905217, 3.277574244, + 7.168351788, 3.076362594, + 7.477905811, 3.602304591, + 6.728719376, 3.695603676, + 7.095775599, 2.990707956, + 6.150361244, 3.45383655, + 6.943060644, 3.968941239, + + 6.803567408, 4.121564093, // 2 + 6.762121615, 3.087711578, + 6.465287869, 3.603133507, + 7.611023558, 3.417313085, + 7.189842199, 3.452137784, + 6.765406578, 3.858941034, + 6.399895618, 3.180273849, + 7.014705581, 3.397562373, + 6.918960683, 3.902484701, + 7.031672773, 3.921038089, + 7.214279867, 3.871272437, + 6.812644549, 3.93849649, + 7.437718742, 3.430360834, + 6.426206021, 3.655662212, + 6.648560142, 4.069987106, + 6.88743118, 4.208876563, + 6.655089134, 2.839804335, + 7.370126282, 3.182494525, + 6.887871221, 4.117920956, + 7.158322929, 3.859790417, + 6.364712745, 3.509128308, + 7.11236415, 4.188951882, + 7.620591907, 3.686117194, + 6.613049796, 3.106981314, + 6.67876952, 3.135369123, + + 7.703022985, 2.273593401, + 7.898636894, 2.023189177, + 8.957909495, 2.593391187, + 8.4808326, 2.009292044, + 8.395710105, 2.196289322, + 7.728504472, 2.153318733, + 7.804590761, 1.888741071, + 8.253832457, 2.471540555, + 7.74772304, 2.106766409, + 8.227307149, 2.22559815, + 8.947072187, 2.44148468, + 7.767299781, 2.719099859, + 7.707566671, 2.327217094, + 8.220829325, 2.41692421, + 8.008964572, 1.947041487, + 8.007040668, 2.736097751, + 6.919912401, 2.227266215, + 9.063468348, 2.295595489, + 8.529063223, 2.233897542, + 6.993174236, 1.654393254, + 7.576853804, 1.963363966, + 8.189054217, 2.569414028, + 8.834564768, 2.425919483, + 8.756513641, 2.282261201, + 8.295544321, 2.359585747, + + 7.536339262, 2.543369696, + 8.696934035, 2.53204118, + 7.552938046, 2.59844655, + 7.163521562, 2.516844389, + 7.477762541, 1.937401502, + 7.392609345, 2.293323646, + 8.64426206, 2.745164657, + 8.630385395, 2.515554964, + 7.939417508, 2.336437504, + 8.39703023, 2.237479277, + 7.707566671, 2.074940134, + 7.824750814, 2.373155406, + 7.917927096, 1.942160093, + 8.414447696, 2.745164657, + 8.177418695, 2.198489531, + 7.886049653, 2.674502139, + 8.094250803, 2.104873207, + 8.563232977, 2.194764526, + 7.351961768, 1.899363474, + 7.366687816, 2.068605579, + 7.109201989, 2.697957388, + 7.882181379, 2.258601281, + 8.429572852, 2.414498864, + 7.40329315, 1.870474222, + 6.053715794, 2.136822285, + + 7.504789292, 3.371231503, + 7.97874496, 3.435917641, + 6.439796148, 2.281278782, + 6.614850897, 2.863505188, + 6.757659797, 2.868632186, + 5.652684255, 2.329847111, + 6.550737837, 2.571870075, + 6.65383041, 2.887553982, + 7.1010254, 3.304304222, + 7.364415972, 2.531887677, + 7.304508893, 2.484721341, + 7.03465073, 3.036983974, + 6.986297304, 2.726652203, + 6.794203729, 2.873728484, + 6.815387134, 2.826664484, + 7.29040709, 3.167451237, + 7.130057922, 2.869737408, + 6.888884341, 2.765068872, + 6.134509507, 2.853834503, + 7.033750179, 3.484741808, + 7.027036984, 3.345207638, + 7.515012587, 3.329755009, + 7.722630426, 3.087476207, + 7.469688287, 2.565208048, + 6.986297304, 3.411285536, + + 7.256841114, 3.087445506, + 7.753904091, 3.000624245, + 7.19390491, 2.825375059, + 7.116119855, 2.977199697, + 6.811672363, 2.557717104, + 7.20332999, 2.816400254, + 6.794142328, 2.744079903, + 5.941075339, 2.595867701, + 7.638367547, 2.924558423, + 7.523199411, 2.492918398, + 7.644978407, 2.457408052, + 6.337624593, 2.375161178, + 6.740610737, 2.921529299, + 6.834841073, 3.042643116, + 6.80018011, 3.117603717, + 6.606633374, 3.06237336, + 7.791174604, 3.493153769, + 6.886049653, 2.910927362, + 6.575861151, 2.607892098, + 7.07455126, 2.631275712, + 6.152305614, 2.864538775, + 7.746658753, 2.811109519, + 6.858337256, 2.856444053, + 6.674757977, 2.902699605, + 7.309861029, 2.836324935, + + 4.884975133, 2.565238748, + 5.290642461, 2.620673776, + 4.862758141, 2.100483023, + 4.912247488, 2.476012608, + 5.119097812, 2.362154363, + 5.403579688, 2.810945783, + 4.894870955, 2.852268773, + 5.047954318, 2.326367711, + 5.253279846, 2.565351317, + 4.858746597, 2.59479318, + 5.479021265, 2.48573446, + 4.768333367, 2.376102663, + 4.918305737, 2.044751223, + 5.234112446, 2.346138889, + 4.494146421, 1.968337461, + 4.946918684, 2.278669232, + 4.901185043, 2.360690968, + 5.248316584, 2.052201232, + 4.948494648, 2.156941403, + 5.328424651, 2.597658569, + 5.197415011, 2.522759369, + 5.579217749, 2.475112057, + 5.253371948, 2.606172865, + 5.368632186, 2.198868172, + 4.92802759, 2.238400295, + + 5.868335414, 2.757209521, + 4.782680775, 2.42910211, + 4.864426206, 2.785157289, + 5.970476268, 2.72406312, + 5.326858921, 1.832507829, + 5.182351256, 2.711588448, + 5.529759103, 2.331013733, + 5.035766184, 2.212509466, + 4.953099736, 2.574479625, + 5.142972636, 2.623365194, + 4.93849649, 2.718997523, + 4.942722937, 2.30922655, + 5.034967969, 2.646155263, + 4.93849649, 2.348318631, + 5.145848257, 2.432049367, + 4.896702757, 2.162068401, + 5.368212612, 2.529022289, + 5.186495835, 2.479318038, + 5.026064799, 2.495456313, + 5.371548742, 2.170582697, + 5.420454778, 2.88029841, + 5.203340224, 2.378916883, + 4.776233652, 2.248357519, + 5.08554207, 2.64475327, + 5.318743732, 2.226764772, + + 6.377586524, 2.622198571, + 6.014736282, 2.593462822, + 6.935283162, 2.401502282, + 6.777062568, 2.784727481, + 6.838709347, 2.726652203, + 6.568380442, 2.388290796, + 5.97265601, 2.575666714, + 6.445158517, 2.750179087, + 6.601342639, 2.506651794, + 6.150780818, 2.714064962, + 6.69355697, 2.793170143, + 7.099776909, 2.516844389, + 7.121697128, 3.028080804, + 6.223316073, 2.383143331, + 6.213747723, 2.75676948, + 5.843058597, 2.4721034, + 6.576915205, 3.044270247, + 6.848349332, 2.567602693, + 7.083495364, 2.757076485, + 6.216060501, 2.967682515, + 6.841155161, 2.492355554, + 6.477783008, 2.142614462, + 6.567694795, 2.483370515, + 6.417722426, 2.638510817, + 6.855635604, 2.783652961, + + 6.082789251, 2.419769132, + 6.18482777, 2.5, + 6.389457418, 3.207546204, + 7.013590127, 2.701958697, + 6.092275732, 2.677183323, + 6.358562394, 2.265386111, + 6.470813975, 2.520763831, + 6.829181932, 2.689811498, + 6.183159704, 2.509619517, + 6.262663992, 2.640342619, + 6.667348902, 2.542346344, + 6.904705377, 2.701651692, + 7.477609038, 2.578992611, + 6.823788862, 2.86011789, + 6.00629362, 2.293323646, + 6.243332856, 1.989367363, + 6.59548906, 2.619814159, + 7.123385661, 2.397787511, + 6.920772017, 2.928119691, + 7.180243149, 2.413997421, + 6.952925766, 2.678851389, + 5.800794122, 2.394983524, + 7.165752471, 2.624961624, + 6.765416812, 2.807793856, + 6.048773, 2.625780307 + }; + + public static double[][][] structData = new double[10][][]; // [image№][seed№][length/width] + + static { + for(int imageNum = 0; imageNum<10; imageNum++) { + structData[imageNum] = new double[25][]; + + for(int seedNum = 0; seedNum<25; seedNum++) { + structData[imageNum][seedNum] = new double[2]; + + structData[imageNum][seedNum][0] = arrData[(imageNum+1)*(seedNum+1)*(2)-2]; // length + structData[imageNum][seedNum][1] = arrData[(imageNum+1)*(seedNum+1)*(2)-1]; // width + } + } + } + + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/TestMain.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/TestMain.java new file mode 100644 index 0000000..582ba1e --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/TestMain.java @@ -0,0 +1,128 @@ +package org.wheatdb.seedcounter.processor.test; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.imgcodecs.Imgcodecs; +import org.wheatdb.seedcounter.desktop.Display; + +/** + * Main class of test application. + */ +public class TestMain { + private static File imgSrcDir = ImgDirs.SonyL4; + + private static File imgOutDir = new File(imgSrcDir, "out"); + + private static List inputImgs = new LinkedList(); + private static List markedImgs = new LinkedList(); + private static List outputImgs = new LinkedList(); + + private static int fromImg = 1; + private static int toImg = 10; + + static { + if(!imgOutDir.exists()) { + imgOutDir.mkdirs(); + } + } + + + public static void main(String[] args) { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + + linkImage(); + doTests(); + //display(outputImgs); + } + + + /** + * Execute tests. + */ + private static void doTests() { + /* + DetectionTester detectTester = new DetectionTester(new DetectionProcessor(SheetFormat.A4), inputImgs, markedImgs, outputImgs); + + double appraisal = detectTester.doTests(); + + System.out.println("Appraisal: " + appraisal); + */ + } + + + /** + * Link to source, marked and output image files. + */ + private static void linkImage() { + // download source and marked image + for(int i=fromImg; i images) { + for(File out : images) { + Display display = new Display(); + + Mat img = Imgcodecs.imread(out.getAbsolutePath()); + display.setImage(img); + } + } +} + + +class ImgDirs { + public static File SonyL1 = new File("../data/seedcounter/estimate/Sony/L1"); + public static File SonyL2 = new File("../data/seedcounter/estimate/Sony/L2"); + public static File SonyL3 = new File("../data/seedcounter/estimate/Sony/L3"); + public static File SonyL4 = new File("../data/seedcounter/estimate/Sony/L4"); + public static File SonyDark = new File("../data/seedcounter/estimate/Sony/dark"); + public static File SonyOutsideSun = new File("../data/seedcounter/estimate/Sony/outside_sun"); + + public static File SamsungL1 = new File("../data/seedcounter/estimate/Samsung/L1"); + public static File SamsungL2 = new File("../data/seedcounter/estimate/Samsung/L2"); + public static File SamsungL3 = new File("../data/seedcounter/estimate/Samsung/L3"); + public static File SamsungL4 = new File("../data/seedcounter/estimate/Samsung/L4"); + public static File SamsungDark = new File("../data/seedcounter/estimate/Samsung/dark"); + public static File SamsungOutsideSun = new File("../data/seedcounter/estimate/Samsung/outside_sun"); + + public static File DNSL1 = new File("../data/seedcounter/estimate/DNS/L1"); + public static File DNSL2 = new File("../data/seedcounter/estimate/DNS/L2"); + public static File DNSL3 = new File("../data/seedcounter/estimate/DNS/L3"); + public static File DNSL4 = new File("../data/seedcounter/estimate/DNS/L4"); + public static File DNSDark = new File("../data/seedcounter/estimate/DNS/dark"); + public static File DNSOutsideSun = new File("../data/seedcounter/estimate/DNS/outside_sun"); + + public static File Lenovo = new File("../data/seedcounter/estimate/Lenovo"); + + public static File testWatersheld = new File("../data/seedcounter/test/watersheld"); +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/testers/CalibrationDisplay.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/testers/CalibrationDisplay.java new file mode 100644 index 0000000..c71b5c7 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/testers/CalibrationDisplay.java @@ -0,0 +1,93 @@ +package org.wheatdb.seedcounter.processor.test.testers; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; + +import org.opencv.core.Scalar; +import org.wheatdb.seedcounter.desktop.Display; +import org.wheatdb.seedcounter.processor.Calibration; + +public class CalibrationDisplay extends Display implements MouseListener { + private static final long serialVersionUID = 1L; + private static final int COLORS_NUM = 5; + + List colors = new ArrayList(10); + + + public CalibrationDisplay() { + addMouseListener(this); + } + + @Override + public void mouseClicked(MouseEvent e) { + BufferedImage screenshot = getScreenShot(); + + int[] pixel = new int[3]; + screenshot.getData().getPixel(e.getX(), e.getY(), pixel); + + synchronized (colors) { + colors.add(new Scalar(pixel[0], pixel[1], pixel[2])); + + if(COLORS_NUM==colors.size()) { + colors.notify(); + } + } + } + + public synchronized Calibration getCalibration() { + synchronized (colors) { + try { + while(COLORS_NUM>colors.size()) + colors.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + Calibration calibration = new Calibration(); + + for(Scalar color : colors) { + calibration.addColor(Calibration.RGBtoHSV(color, true)); + } + + return calibration; + } + + @Override + public void mousePressed(MouseEvent e) { + // TODO Auto-generated method stub + + } + + @Override + public void mouseReleased(MouseEvent e) { + // TODO Auto-generated method stub + + } + + @Override + public void mouseEntered(MouseEvent e) { + // TODO Auto-generated method stub + + } + + @Override + public void mouseExited(MouseEvent e) { + // TODO Auto-generated method stub + + } + + public BufferedImage getScreenShot() { + + BufferedImage image = new BufferedImage( + this.getWidth(), + this.getHeight(), + BufferedImage.TYPE_INT_RGB + ); + this.paint( image.getGraphics() ); + return image; + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/testers/FiltersTester.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/testers/FiltersTester.java new file mode 100644 index 0000000..8851a09 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/testers/FiltersTester.java @@ -0,0 +1,87 @@ +package org.wheatdb.seedcounter.processor.test.testers; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.MatOfFloat; +import org.opencv.core.MatOfInt; +import org.opencv.core.Point; +import org.opencv.imgproc.Imgproc; +import org.wheatdb.seedcounter.processor.filters.GrayBinarization; + +public class FiltersTester { + public Mat doTests(Mat img) { + Mat outImg = binarizationFilterTest(img); + + return outImg; + } + + public Mat binarizationFilterTest(Mat srcImg) { + //Calibration calibration = calibrate(srcImg); + + GrayBinarization binarization = new GrayBinarization(false, srcImg.height()*srcImg.width()/(297*210*4/3)); + //HSVBinarization binarization = new HSVBinarization(calibration.targetColor); + + Mat outImg = binarization.apply(srcImg.clone()); + + //FrameProcessor fp = new FrameProcessor(); + //Mat outImg = fp.treshHullFilterLines(srcImg, false); + + return outImg; + } + + + @SuppressWarnings("unused") + public static Mat adjastImage(Mat img) { + Mat histB = histogram(img, 0); + Mat histG = histogram(img, 1); + Mat histR = histogram(img, 2); + + List rgb = new LinkedList(); Core.split(img, rgb); + + HistAnalyzer histAnalyzerR = new HistAnalyzer(histR); + HistAnalyzer histAnalyzerG = new HistAnalyzer(histG); + HistAnalyzer histAnalyzerB = new HistAnalyzer(histB); + + contrastAndBrightAdjast(rgb.get(0), histAnalyzerR); + contrastAndBrightAdjast(rgb.get(1), histAnalyzerG); + contrastAndBrightAdjast(rgb.get(2), histAnalyzerB); + + Mat out = new Mat(); Core.merge(rgb, out); + return out; + } + + + private static void contrastAndBrightAdjast(Mat src, HistAnalyzer histAnalyzer) { + int histLenght = histAnalyzer.getHistLength(); + int leftCenter = histAnalyzer.getLeftCenterPos(); + int leftWidth = histAnalyzer.getLeftWidth(); + int rightCenter = histAnalyzer.getRightCenterPos(); + int rightWidth = histAnalyzer.getRightWidth(); + + + float contrast = (float)(histLenght/(float)((rightCenter+rightWidth/2)-(leftCenter-leftWidth/2))); + float bright = -(float)((256.0/histLenght)*leftCenter*contrast*0.4); + + Mat kernel2 = new MatOfFloat(contrast); + Imgproc.filter2D(src, src, -1, kernel2, new Point(), bright); + } + + + private static Mat histogram(Mat img, int channel) { + MatOfInt channels = new MatOfInt(channel); + + MatOfInt histSize = new MatOfInt(256); + MatOfFloat ranges=new MatOfFloat(0, 256); + + /// Compute the histograms: + Mat hist = new Mat(); + Imgproc.calcHist(Arrays.asList(img, img, img), channels, new Mat(), hist, histSize, ranges); + Core.normalize(hist, hist, 256, 0, Core.NORM_INF); + + return hist; + } +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/testers/HistAnalyzer.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/testers/HistAnalyzer.java new file mode 100644 index 0000000..f27da01 --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/processor/test/testers/HistAnalyzer.java @@ -0,0 +1,243 @@ +package org.wheatdb.seedcounter.processor.test.testers; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.opencv.core.Mat; + + +/** + * It's analyzer for image histograms. + * @author Komyshev + * @version 0.1 + */ +public class HistAnalyzer { + public Mat hist; // histogram for analyzing + private int histLength; + + private static final float persentOfNonSignificantMinValues = 0.1f; // 10% from average value of histogram + private double nonSignHistValueThres; // histogram values under non-significant threshold must be deleted + + private double sumValue; // sum values of histogram + private double averageValue; // average value of histogram + + private int leftLimitPos; // minimal by position(left) position of significant histogram value + private int rightLimitPos; // maximal by position(right) position of significant histogram value + + private int leftCenterPos; // center of left contrast part + private int leftWidth; // significant width of left contrast part + + private int rightCenterPos; // center of right contrast part + private int rightWidth; // significant width of right contrast part + + // center of contrast division. + private int centerPos; // it is on non-significant area between contrast parts. it will be new histogram center of future image. + + + + HistAnalyzer(Mat hist) { + this.hist = hist; + histLength = hist.cols()*hist.rows(); + + sumValue = getSumValues(); + + averageValue = getAverageValue(); + + nonSignHistValueThres = averageValue*persentOfNonSignificantMinValues; + + determLimits(nonSignHistValueThres); + + detectSideCenterPos(sumValue); + + determCenterPos(leftCenterPos, leftWidth, rightCenterPos, rightWidth); + } + + + /** + * Detect centers and widths of right and left local highs in histogram. + * @param leftLim + * @param rightLim + * @param average + * @param sum + */ + private void detectSideCenterPos(double sum) { + HistIterator histIter = new HistIterator(); + + // getting big-half side of maximal histogram values + int massCenterPos = 0; + int massAccum = 0; + List positionCollection = new LinkedList(); + while(positionCollection.size()!=histLength) { + int curPosition = histIter.getNextPosOfMaxValue(); + double curValue = hist.get(curPosition, 0)[0]; + + massCenterPos = (int)(massCenterPos*(massAccum/(massAccum+curValue))+curPosition*(curValue/(massAccum+curValue))); + + massAccum +=curValue; + positionCollection.add(curPosition); + + if(massAccum>=(sum) && positionCollection.size()>=2) { + break; + } + } + + Collections.sort(positionCollection); + + // divide to left and right side by positions of values + List leftPositions = new LinkedList(); + List rightPositions = new LinkedList(); + while(true) { + if(!positionCollection.isEmpty()) { + int pos = positionCollection.get(0); + if(pos>massCenterPos) { + rightPositions.add(positionCollection.get(0)); + positionCollection.remove(0); + } else { + leftPositions.add(positionCollection.get(0)); + positionCollection.remove(0); + } + + } + else break; + } + + // determining of center of left and right positions + for(Integer cur : leftPositions) { + leftCenterPos +=cur; + } + leftCenterPos /= leftPositions.size(); + leftWidth = Collections.max(leftPositions)-Collections.min(leftPositions); + + for(Integer cur : rightPositions) { + rightCenterPos +=cur; + } + rightCenterPos /= rightPositions.size(); + rightWidth = Collections.max(rightPositions)-Collections.min(rightPositions); + } + + + private void determCenterPos(int leftCenterPos, int leftWidth, int rightCenterPos, int rightWidth) { + double normLeftWidth = (double)leftWidth/((double)(leftWidth+rightWidth)); + double normRightWidth = (double)rightWidth/((double)(leftWidth+rightWidth)); + centerPos = (int)Math.round(leftCenterPos*normLeftWidth + rightCenterPos*normRightWidth); + } + + + private void determLimits(double threshold) { + // to establish left limit + for(int i=0; i sortedPosHist; // sorted position of histogram by value of histogram + + private HistIterator() { + // forming sorted histogram + List initHistValues = new LinkedList(); + for(int i=0; i reducedHistValues = new LinkedList(); + reducedHistValues.addAll(initHistValues); + + sortedPosHist = new LinkedList(); + while(!reducedHistValues.isEmpty()) { + Double curMin = Collections.min(reducedHistValues); + int posMin = initHistValues.indexOf(curMin); + reducedHistValues.remove(curMin); + + sortedPosHist.add(posMin); + } + + curPosOfPosOfMinHistVal = 0; + curPosOfPosOfMaxHistVal = sortedPosHist.size()-1; + } + + public boolean hasNextPosOfMinValue() { + if(curPosOfPosOfMinHistVal=0) + return true; + return false; + } + + public int getNextPosOfMinValue() { + return sortedPosHist.get(curPosOfPosOfMinHistVal++); + } + + public int getNextPosOfMaxValue() { + return sortedPosHist.get(curPosOfPosOfMaxHistVal--); + } + } + +} diff --git a/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/server/DefaultFilesAndDirs.java b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/server/DefaultFilesAndDirs.java new file mode 100644 index 0000000..3af5a8e --- /dev/null +++ b/seed-counter-desktop/src/main/java/org/wheatdb/seedcounter/server/DefaultFilesAndDirs.java @@ -0,0 +1,24 @@ +package org.wheatdb.seedcounter.server; + +import java.io.File; + +/** + * Директории и файлы по умолчанию. + * @author Komyshev + */ +public class DefaultFilesAndDirs { + // Директория входных/выходных данных + public static final File dataDir = new File("../data/seedcounter"); + + // Директория входных файлов + public static final File inputDir = new File(dataDir, "input/"); + + // Директория выходных файлов + public static final File outputDir = new File(dataDir, "output/"); + + // Директория выходных файлов + public static final File markedDir = new File(dataDir, "marked/"); + + // Директория шаблонов для распознавания + public static final File templateDir = new File(dataDir, "templates/"); +} diff --git a/seed-counter-desktop/src/main/java/ru/delkom07/recognition/potatoes/PotatoType.java b/seed-counter-desktop/src/main/java/ru/delkom07/recognition/potatoes/PotatoType.java new file mode 100644 index 0000000..f6a6bd5 --- /dev/null +++ b/seed-counter-desktop/src/main/java/ru/delkom07/recognition/potatoes/PotatoType.java @@ -0,0 +1,7 @@ +package ru.delkom07.recognition.potatoes; + + + +public enum PotatoType { + WHITE, DARK; +} diff --git a/seed-counter-desktop/src/main/java/ru/delkom07/recognition/potatoes/refactor/DetectionProcessor.java b/seed-counter-desktop/src/main/java/ru/delkom07/recognition/potatoes/refactor/DetectionProcessor.java new file mode 100644 index 0000000..d6e819b --- /dev/null +++ b/seed-counter-desktop/src/main/java/ru/delkom07/recognition/potatoes/refactor/DetectionProcessor.java @@ -0,0 +1,106 @@ +package ru.delkom07.recognition.potatoes.refactor; + +import java.util.ArrayList; +import java.util.List; + +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; +import org.wheatdb.seedcounter.SeedDataContainer; +import org.wheatdb.seedcounter.processor.ElementsProcessor; +import org.wheatdb.seedcounter.processor.PlanarObject; +import org.wheatdb.seedcounter.processor.SeedCounterProcessorException; +import org.wheatdb.seedcounter.processor.filters.GrayBinarization; +import org.wheatdb.seedcounter.processor.filters.HSVBinarization; + +import ru.delkom07.recognition.potatoes.training.PotatoBinarizationTraining; +import ru.delkom07.util.Pair; + +public class DetectionProcessor { + + /** + * Detect contours of potatoes in image. + * @param image - source image. + * @return list of contours. + */ +// @Deprecated +// public List detectPotatoesContours(Mat image, double pixPerMM) throws SeedCounterProcessorException { +// List contours = new ArrayList(); +// synchronized (DetectionProcessor.class) { +// List> targetsAndRanges = PotatoBinarizationTraining.getCommonPotatoesHSVPreset2().get(0).getParams(); // differ +// HSVBinarization seedContourBinarization = new HSVBinarization(targetsAndRanges); // differ +// +// Mat hulled = seedContourBinarization.apply(image); +// +// // Find contours +// Imgproc.findContours(hulled, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); +// hulled.release(); +// } +// +// +// contours = ElementsProcessor.filterCircularContours(contours, 8*pixPerMM, 100*pixPerMM); // differ +// +// // Gauge seeds +// List seedDataList = new ArrayList(contours.size()); +// for(int i=0; i detectPotatoesContours(Mat image) throws SeedCounterProcessorException { +// // init size +// //prepareSize(image.cols(), image.rows()); +// if(-1 == widthPadding || -1 == heightPadding) +// throw new SeedCounterProcessorException("Padding isn't initialized."); +// +// +// int width = image.cols(); +// int height = image.rows(); +// +// // Clean +// GrayBinarization binarization = new GrayBinarization(true, width*height/(210*297)); // ????????????????????????????? +// Mat hulled = binarization.apply(image); +// +// // Find contours +// List contours = new ArrayList(); +// Imgproc.findContours(hulled, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); +// +// /* // disabled for desktop +// if(SeedCounterApplication.config.getUseWatershed()) { +// double maxSeedAreaInMM2 = 78; +// double maxSeedAreaInPix = maxSeedAreaInMM2 * ((width+2*widthPadding)/mmInBigSideOfSheet); +// contours = clarifyContours(contours, maxSeedAreaInPix); +// } +// */ +// +// // Filter +// double pixInMM = (width+2*widthPadding)/mmInBigSideOfSheet; +// //contours = ElementsProcessor.filterCircularContours(contours, SeedCounterApplication.config.getMinSeedLength()*pixInMM, SeedCounterApplication.config.getMaxSeedLength()*pixInMM); +// contours = ElementsProcessor.filterCircularContours(contours, 8*pixInMM, 100*pixInMM); +// +// // Gauge seeds +// List seedDataList = new ArrayList(contours.size()); +// for(int i=0; i> params; + Double jIndex = -1.0; + + + + HSVParameters(List> params) { + this.params = params; + } + + HSVParameters(List> params, Double jIndex) { + this.params = params; + this.jIndex = jIndex; + } + + public List> getParams() { + return params; + } + + public Double getJIndex() { + return jIndex; + } + + public void setJIndex(Double jIndex) { + this.jIndex = jIndex; + } + + @Override + public String toString() { + return "\nJaccard Index: " + jIndex + "\n" + "Parameters: " + params.toString(); + } +} \ No newline at end of file diff --git a/seed-counter-desktop/src/main/java/ru/delkom07/recognition/potatoes/training/LMTraining.java b/seed-counter-desktop/src/main/java/ru/delkom07/recognition/potatoes/training/LMTraining.java new file mode 100644 index 0000000..dc08820 --- /dev/null +++ b/seed-counter-desktop/src/main/java/ru/delkom07/recognition/potatoes/training/LMTraining.java @@ -0,0 +1,198 @@ +package ru.delkom07.recognition.potatoes.training; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.math3.fitting.leastsquares.LeastSquaresBuilder; +import org.apache.commons.math3.fitting.leastsquares.LeastSquaresOptimizer; +import org.apache.commons.math3.fitting.leastsquares.LeastSquaresProblem; +import org.apache.commons.math3.fitting.leastsquares.LevenbergMarquardtOptimizer; +import org.apache.commons.math3.fitting.leastsquares.MultivariateJacobianFunction; +import org.apache.commons.math3.geometry.euclidean.twod.Vector2D; +import org.apache.commons.math3.linear.Array2DRowRealMatrix; +import org.apache.commons.math3.linear.ArrayRealVector; +import org.apache.commons.math3.linear.RealMatrix; +import org.apache.commons.math3.linear.RealVector; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.Scalar; +import org.opencv.imgcodecs.Imgcodecs; +import org.wheatdb.seedcounter.processor.filters.HSVBinarization; +import org.wheatdb.seedcounter.server.DefaultFilesAndDirs; + +import ru.delkom07.util.Pair; + + + +public class LMTraining { + final Vector2D[] observedPoints; + public double rms; + + public static void main(String[] args) throws IOException { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + + File inputDir = DefaultFilesAndDirs.inputDir; + File markedDir = DefaultFilesAndDirs.markedDir; + + File[] inputImgFiles = inputDir.listFiles(new PotatoBinarizationTraining.ImageFilter()); + + List inputs = new ArrayList(); + List markets = new ArrayList(); + + for(File inputImgFile: inputImgFiles) { + File markedImgFile = new File(markedDir, PotatoBinarizationTraining.changeExtension(inputImgFile.getName(), "png")); + + Mat input = Imgcodecs.imread(inputImgFile.getCanonicalPath()); + Mat marked = Imgcodecs.imread(markedImgFile.getCanonicalPath()); + + /* + Size newSize = new Size(input.size().width/8, input.size().height/8); + Imgproc.resize(input, input, newSize); + Imgproc.resize(marked, marked, newSize); + */ + + inputs.add(input); + markets.add(marked); + } + + LMTraining training = new LMTraining(inputs, markets); + + for(int i=0; i imgs, List marked) { + observedPoints = new Vector2D[imgs.size()]; + for(int i=0; i value(final RealVector currentParam) { + + double h = currentParam.getEntry(0); + double s = currentParam.getEntry(1); + double v = currentParam.getEntry(2); + //double rh = currentParam.getEntry(3); + //double rs = currentParam.getEntry(4); + //double rv = currentParam.getEntry(5); + + RealVector value = new ArrayRealVector(observedPoints.length); + RealMatrix jacobian = new Array2DRowRealMatrix(observedPoints.length, 3); + + + for (int i = 0; i < observedPoints.length; ++i) { + //Random rand = new Random(); + double delta = 1; + + double val = function(imgs, marked, h, s, v); + + double derivativeH = (function(imgs, marked, h+delta, s, v)-function(imgs, marked, h, s, v))/delta; + double derivativeS = (function(imgs, marked, h, s+delta, v)-function(imgs, marked, h, s, v))/delta; + double derivativeV = (function(imgs, marked, h, s, v+delta)-function(imgs, marked, h, s, v))/delta; + + //double derivativeRH = (function(imgs, marked, h, s, v, rh+delta, rs, rv)-function(imgs, marked, h, s, v, rh, rs, rv))/delta; + //double derivativeRS = (function(imgs, marked, h, s, v, rh, rs+delta, rv)-function(imgs, marked, h, s, v, rh, rs, rv))/delta; + //double derivativeRV = (function(imgs, marked, h, s, v, rh, rs, rv+delta)-function(imgs, marked, h, s, v, rh, rs, rv))/delta; + + value.setEntry(i, val); + + jacobian.setEntry(i, 0, derivativeH); + jacobian.setEntry(i, 1, derivativeS); + jacobian.setEntry(i, 2, derivativeV); + + //jacobian.setEntry(i, 3, derivativeRH); + //jacobian.setEntry(i, 4, derivativeRS); + //jacobian.setEntry(i, 5, derivativeRV); + + } + + return new org.apache.commons.math3.util.Pair(value, jacobian); + } + }; + + double[] zeros = new double[observedPoints.length]; + Arrays.fill(zeros, 1.0); // prev version + + // least squares problem to solve : modeled radius should be close to target radius + LeastSquaresProblem problem = new LeastSquaresBuilder(). + start(new double[] {10, 244, 74}). + model(distances). + target(zeros). + lazyEvaluation(false). + maxEvaluations(1000_000). + maxIterations(1000_000). + build(); + + LevenbergMarquardtOptimizer optimazer = new LevenbergMarquardtOptimizer(); + + try { + LeastSquaresOptimizer.Optimum optimum = optimazer.optimize(problem); + + /* + double H = optimum.getPoint().getEntry(0); + double S = optimum.getPoint().getEntry(1); + double V = optimum.getPoint().getEntry(2); + + double RH = optimum.getPoint().getEntry(3); + double RS = optimum.getPoint().getEntry(4); + double RV = optimum.getPoint().getEntry(5); + + + rms = optimum.getRMS(); + */ + + //System.out.println(rms); + + } catch(org.apache.commons.math3.exception.TooManyEvaluationsException ex) { + System.out.println("Too many evaluations for Levenberg-Marquardt optimizer. Trapeze parameters will be calculating using brute-force method."); + } + } + + + + + private double function(List imgs, List marked, double h, double s, double v) { + List> parameters = new LinkedList>(); + + Scalar target1 = new Scalar(h, s, v); + Scalar range1 = new Scalar(20, 200, 200); + parameters.add(new Pair(target1, range1)); + + HSVBinarization hsv = new HSVBinarization(parameters); + + double sumJIndex = 0; + + for(int i=0; i outsidePoints = new LinkedList(); + for(int i=contourRect.y; i 0 ? (int)skeleton.get(i-1, j)[0] & 0xff : 0; + fourNeighbors[1] = j+1 < src.cols() ? (int)skeleton.get(i, j+1)[0] & 0xff : 0; + fourNeighbors[2] = i+1 < src.rows() ? (int)skeleton.get(i+1, j)[0] & 0xff : 0; + fourNeighbors[3] = j-1 > 0 ? (int)skeleton.get(i, j-1)[0] & 0xff : 0; + + for(int neighbor : fourNeighbors) { + if(neighbor == 0) { + outsidePoints.add(new Point(j, i)); + break; + } + } + } + } + } + + for(Point removingPoint : outsidePoints) { + if(!isTearsUpArea(skeleton, removingPoint)) { + skeleton.put((int)removingPoint.y, (int)removingPoint.x, (byte)0); + } + } + + } + + return skeleton; + } + + + /** + * Разрывает ли область удаление данной точки с изображения. + * @param img + * @param point + * @return true, если разрывает; false, если нет. + */ + public static boolean isTearsUpArea(Mat img, Point point) { + int pointRow = (int)point.y; + int pointCol = (int)point.x; + + boolean[] neighbors = new boolean[9]; + + // считывание соседний пикслелей + neighbors[0] = img.get(pointRow-1, pointCol-1)[0] != 0.0 ? true : false; + neighbors[1] = img.get(pointRow-1, pointCol )[0] != 0.0 ? true : false; + neighbors[2] = img.get(pointRow-1, pointCol+1)[0] != 0.0 ? true : false; + + neighbors[3] = img.get(pointRow, pointCol-1)[0] != 0.0 ? true : false; + neighbors[4] = img.get(pointRow, pointCol+1)[0] != 0.0 ? true : false; + + neighbors[5] = img.get(pointRow+1, pointCol-1)[0] != 0.0 ? true : false; + neighbors[6] = img.get(pointRow+1, pointCol)[0] != 0.0 ? true : false; + neighbors[7] = img.get(pointRow+1, pointCol+1)[0] != 0.0 ? true : false; + + // вертикальное разделение + if(!neighbors[1] && !neighbors[6] && (neighbors[0] || neighbors[3] || neighbors[5]) && (neighbors[2] || neighbors[4] || neighbors[7])) + return true; + + // горизонтальное разделение + if(!neighbors[3] && !neighbors[4] && (neighbors[0] || neighbors[1] || neighbors[2]) && (neighbors[5] || neighbors[6] || neighbors[7])) + return true; + + // случаи изолирования диагональных пикслелей + // верхний левый + if(!neighbors[3] && !neighbors[1] && (neighbors[5] || neighbors[6] || neighbors[7] || neighbors[4] || neighbors[2]) && neighbors[0]) + return true; + + // нижний правый + if(!neighbors[6] && !neighbors[4] && (neighbors[5] || neighbors[3] || neighbors[0] || neighbors[1] || neighbors[2]) && neighbors[7]) + return true; + + // нижний левый + if(!neighbors[3] && !neighbors[6] && (neighbors[0] || neighbors[1] || neighbors[2] || neighbors[4] || neighbors[7]) && neighbors[5]) + return true; + + // верхний правый + if(!neighbors[1] && !neighbors[4] && (neighbors[0] || neighbors[3] || neighbors[5] || neighbors[6] || neighbors[7]) && neighbors[2]) + return true; + + return false; + } + + + /** + * Разрывает ли область удаление данной точки с изображения. + * @param img + * @param point + * @return true, если разрывает; false, если нет. + */ + public static boolean isReduceSkeleton(Mat img, Point point) { + int pointRow = (int)point.y; + int pointCol = (int)point.x; + + boolean[] neighbors = new boolean[9]; + + // считывание соседний пикслелей + neighbors[0] = img.get(pointRow-1, pointCol-1)[0] != 0.0 ? true : false; + neighbors[1] = img.get(pointRow-1, pointCol )[0] != 0.0 ? true : false; + neighbors[2] = img.get(pointRow-1, pointCol+1)[0] != 0.0 ? true : false; + + neighbors[3] = img.get(pointRow, pointCol-1)[0] != 0.0 ? true : false; + neighbors[4] = img.get(pointRow, pointCol+1)[0] != 0.0 ? true : false; + + neighbors[5] = img.get(pointRow+1, pointCol-1)[0] != 0.0 ? true : false; + neighbors[6] = img.get(pointRow+1, pointCol)[0] != 0.0 ? true : false; + neighbors[7] = img.get(pointRow+1, pointCol+1)[0] != 0.0 ? true : false; + + // укорачивание остова толщиной в 1 пиксель + if(!neighbors[0] && !neighbors[1] && !neighbors[2] && !neighbors[3] && !neighbors[4] && !neighbors[5] && !neighbors[6] && neighbors[7]) { + boolean[] neighbors2 = new boolean[3]; + + neighbors2[0] = img.get(pointRow+2, pointCol+2)[0] != 0.0 ? true : false; + neighbors2[1] = img.get(pointRow+1, pointCol+2 )[0] != 0.0 ? true : false; + neighbors2[2] = img.get(pointRow+2, pointCol+1)[0] != 0.0 ? true : false; + + if(neighbors2[0] && !neighbors2[1] && !neighbors2[2]) { + return true; + } + } + + + if(!neighbors[0] && !neighbors[1] && !neighbors[2] && !neighbors[3] && !neighbors[4] && !neighbors[5] && !neighbors[7] && neighbors[6]) { + boolean[] neighbors2 = new boolean[3]; + + neighbors2[0] = img.get(pointRow+2, pointCol)[0] != 0.0 ? true : false; + neighbors2[1] = img.get(pointRow+2, pointCol-1 )[0] != 0.0 ? true : false; + neighbors2[2] = img.get(pointRow+2, pointCol+1)[0] != 0.0 ? true : false; + + if(neighbors2[0] && !neighbors2[1] && !neighbors2[2]) { + return true; + } + } + + + if(!neighbors[0] && !neighbors[1] && !neighbors[2] && !neighbors[3] && !neighbors[4] && !neighbors[7] && !neighbors[6] && neighbors[5]) { + boolean[] neighbors2 = new boolean[3]; + + neighbors2[0] = img.get(pointRow+2, pointCol-2)[0] != 0.0 ? true : false; + neighbors2[1] = img.get(pointRow+1, pointCol-2)[0] != 0.0 ? true : false; + neighbors2[2] = img.get(pointRow+2, pointCol-1)[0] != 0.0 ? true : false; + + if(neighbors2[0] && !neighbors2[1] && !neighbors2[2]) { + return true; + } + } + + + if(!neighbors[0] && !neighbors[1] && !neighbors[2] && !neighbors[3] && !neighbors[7] && !neighbors[5] && !neighbors[6] && neighbors[4]) { + boolean[] neighbors2 = new boolean[3]; + + neighbors2[0] = img.get(pointRow, pointCol+2)[0] != 0.0 ? true : false; + neighbors2[1] = img.get(pointRow-1, pointCol+2)[0] != 0.0 ? true : false; + neighbors2[2] = img.get(pointRow+1, pointCol+1)[0] != 0.0 ? true : false; + + if(neighbors2[0] && !neighbors2[1] && !neighbors2[2]) { + return true; + } + } + + if(!neighbors[0] && !neighbors[1] && !neighbors[2] && !neighbors[7] && !neighbors[4] && !neighbors[5] && !neighbors[6] && neighbors[3]) { + boolean[] neighbors2 = new boolean[3]; + + neighbors2[0] = img.get(pointRow, pointCol-2)[0] != 0.0 ? true : false; + neighbors2[1] = img.get(pointRow-1, pointCol-2)[0] != 0.0 ? true : false; + neighbors2[2] = img.get(pointRow+1, pointCol-2)[0] != 0.0 ? true : false; + + if(neighbors2[0] && !neighbors2[1] && !neighbors2[2]) { + return true; + } + } + + if(!neighbors[0] && !neighbors[1] && !neighbors[7] && !neighbors[3] && !neighbors[4] && !neighbors[5] && !neighbors[6] && neighbors[2]) { + boolean[] neighbors2 = new boolean[3]; + + neighbors2[0] = img.get(pointRow-2, pointCol+2)[0] != 0.0 ? true : false; + neighbors2[1] = img.get(pointRow-2, pointCol+1)[0] != 0.0 ? true : false; + neighbors2[2] = img.get(pointRow-1, pointCol+2)[0] != 0.0 ? true : false; + + if(neighbors2[0] && !neighbors2[1] && !neighbors2[2]) { + return true; + } + } + + if(!neighbors[0] && !neighbors[7] && !neighbors[2] && !neighbors[3] && !neighbors[4] && !neighbors[5] && !neighbors[6] && neighbors[1]) { + boolean[] neighbors2 = new boolean[3]; + + neighbors2[0] = img.get(pointRow-2, pointCol)[0] != 0.0 ? true : false; + neighbors2[1] = img.get(pointRow-2, pointCol+1)[0] != 0.0 ? true : false; + neighbors2[2] = img.get(pointRow-2, pointCol-1)[0] != 0.0 ? true : false; + + if(neighbors2[0] && !neighbors2[1] && !neighbors2[2]) { + return true; + } + } + + if(!neighbors[7] && !neighbors[1] && !neighbors[2] && !neighbors[3] && !neighbors[4] && !neighbors[5] && !neighbors[6] && neighbors[0]) { + boolean[] neighbors2 = new boolean[3]; + + neighbors2[0] = img.get(pointRow-2, pointCol-2)[0] != 0.0 ? true : false; + neighbors2[1] = img.get(pointRow-1, pointCol-2)[0] != 0.0 ? true : false; + neighbors2[2] = img.get(pointRow-2, pointCol-1)[0] != 0.0 ? true : false; + + if(neighbors2[0] && !neighbors2[1] && !neighbors2[2]) { + return true; + } + } + + return false; + } + + + + /** + * Разрывает ли область удаление данной точки с изображения. + * @param img + * @param point + * @return true, если разрывает; false, если нет. + */ + public static boolean isTearsUpAreaOrReduceSkeleton(Mat img, Point point) { + int pointRow = (int)point.y; + int pointCol = (int)point.x; + + boolean[] neighbors = new boolean[9]; + + // считывание соседний пикслелей + neighbors[0] = (pointRow-1 > 0 && pointCol-1 > 0) ? img.get(pointRow-1, pointCol-1)[0] != 0.0 ? true : false : false; + neighbors[1] = (pointRow-1 > 0) ? img.get(pointRow-1, pointCol )[0] != 0.0 ? true : false : false; + neighbors[2] = (pointRow-1 > 0 && pointCol+1 < img.cols()) ? img.get(pointRow-1, pointCol+1)[0] != 0.0 ? true : false : false; + + neighbors[3] = (pointCol-1 > 0) ? img.get(pointRow, pointCol-1)[0] != 0.0 ? true : false : false; + neighbors[4] = (pointCol+1 < img.cols()) ? img.get(pointRow, pointCol+1)[0] != 0.0 ? true : false : false; + + neighbors[5] = (pointRow+1 < img.rows() && pointCol-1 > 0) ? img.get(pointRow+1, pointCol-1)[0] != 0.0 ? true : false : false; + neighbors[6] = (pointRow+1 < img.rows()) ? img.get(pointRow+1, pointCol)[0] != 0.0 ? true : false : false; + neighbors[7] = (pointRow+1 < img.rows() && pointCol+1 < img.cols()) ? img.get(pointRow+1, pointCol+1)[0] != 0.0 ? true : false : false; + + // вертикальное разделение + if(!neighbors[1] && !neighbors[6] && (neighbors[0] || neighbors[3] || neighbors[5]) && (neighbors[2] || neighbors[4] || neighbors[7])) + return true; + + // горизонтальное разделение + if(!neighbors[3] && !neighbors[4] && (neighbors[0] || neighbors[1] || neighbors[2]) && (neighbors[5] || neighbors[6] || neighbors[7])) + return true; + + // случаи изолирования диагональных пикслелей + // верхний левый + if(!neighbors[3] && !neighbors[1] && (neighbors[5] || neighbors[6] || neighbors[7] || neighbors[4] || neighbors[2]) && neighbors[0]) + return true; + + // нижний правый + if(!neighbors[6] && !neighbors[4] && (neighbors[5] || neighbors[3] || neighbors[0] || neighbors[1] || neighbors[2]) && neighbors[7]) + return true; + + // нижний левый + if(!neighbors[3] && !neighbors[6] && (neighbors[0] || neighbors[1] || neighbors[2] || neighbors[4] || neighbors[7]) && neighbors[5]) + return true; + + // верхний правый + if(!neighbors[1] && !neighbors[4] && (neighbors[0] || neighbors[3] || neighbors[5] || neighbors[6] || neighbors[7]) && neighbors[2]) + return true; + + + // укорачивание остова толщиной в 1 пиксель + if(!neighbors[0] && !neighbors[1] && !neighbors[2] && !neighbors[3] && !neighbors[4] && !neighbors[5] && !neighbors[6] && neighbors[7]) + return true; + + if(!neighbors[0] && !neighbors[1] && !neighbors[2] && !neighbors[3] && !neighbors[4] && !neighbors[5] && !neighbors[7] && neighbors[6]) + return true; + + if(!neighbors[0] && !neighbors[1] && !neighbors[2] && !neighbors[3] && !neighbors[4] && !neighbors[7] && !neighbors[6] && neighbors[5]) + return true; + + if(!neighbors[0] && !neighbors[1] && !neighbors[2] && !neighbors[3] && !neighbors[7] && !neighbors[5] && !neighbors[6] && neighbors[4]) + return true; + + if(!neighbors[0] && !neighbors[1] && !neighbors[2] && !neighbors[7] && !neighbors[4] && !neighbors[5] && !neighbors[6] && neighbors[3]) + return true; + + if(!neighbors[0] && !neighbors[1] && !neighbors[7] && !neighbors[3] && !neighbors[4] && !neighbors[5] && !neighbors[6] && neighbors[2]) + return true; + + if(!neighbors[0] && !neighbors[7] && !neighbors[2] && !neighbors[3] && !neighbors[4] && !neighbors[5] && !neighbors[6] && neighbors[1]) + return true; + + if(!neighbors[7] && !neighbors[1] && !neighbors[2] && !neighbors[3] && !neighbors[4] && !neighbors[5] && !neighbors[6] && neighbors[0]) + return true; + + return false; + } + + + + + + + + + + + + + + + + public static Mat skeletonize0(Mat src, Rect contourRect) { + Mat skeleton = src; //src.clone(); + + List outsidePoints = new LinkedList(); + int iterationsAmount = 100; + for(int k=0; k 0 ? (int)skeleton.get(i-1, j)[0] & 0xff : 0; + fourNeighbors[1] = j+1 < skeleton.cols() ? (int)skeleton.get(i, j+1)[0] & 0xff : 0; + fourNeighbors[2] = i+1 < skeleton.rows() ? (int)skeleton.get(i+1, j)[0] & 0xff : 0; + fourNeighbors[3] = j-1 > 0 ? (int)skeleton.get(i, j-1)[0] & 0xff : 0; + + for(int neighbor : fourNeighbors) { + if(neighbor == 0) { + outsidePoints.add(new Point(j, i)); + break; + } + } + } + } + } + + // Внешние точки удаляются с изображения и их список очищается. + //for(Point removingPoint : outsidePoints) { + /* + for(int l=0; l inputs = new ArrayList(); + List markeds = new ArrayList(); + + for(File inputImgFile: inputImgFiles) { + File markedImgFile = new File(markedDir, changeExtension(inputImgFile.getName(), "png")); + + Mat input = Imgcodecs.imread(inputImgFile.getCanonicalPath()); + Mat marked = Imgcodecs.imread(markedImgFile.getCanonicalPath()); + + + + Size newSize = new Size(input.size().width/8, input.size().height/8); + Imgproc.resize(input, input, newSize); + Imgproc.resize(marked, marked, newSize); + + + inputs.add(input); + markeds.add(marked); + } + + // Training + //List parentPopulation = generateRandomPopulation(300, 3); + //List parentPopulation = getWhitePotatoesHSVPreset(); + //List parentPopulation = getDarkPotatoesHSVPreset2(); + + List parentPopulation = getCommonPotatoesHSVPreset(); + //parentPopulation.addAll(getCommonPotatoesHSVPreset2()); + //parentPopulation.addAll(getWhitePotatoesHSVPreset()); + //parentPopulation.addAll(getWhitePotatoesHSVPreset2()); + //parentPopulation.addAll(getDarkPotatoesHSVPreset()); + //parentPopulation.addAll(getDarkPotatoesHSVPreset2()); + + for(int i=0; i<100; i++) { + System.out.println("Training iteration: " + i); + + List childPopulation = new LinkedList(); + if(i==0) { + childPopulation.addAll(parentPopulation); + } else { + childPopulation.addAll(newGeneration(parentPopulation, 10)); + } + + for(HSVParameters childParam : childPopulation) { + HSVBinarization hsv = new HSVBinarization(childParam.getParams()); + + double middleJIndex = 0; + double sumJIndex = 0; + + for(int imgI=0; imgI() { + @Override + public int compare(HSVParameters arg0, HSVParameters arg1) { + if(arg0.getJIndex() < arg1.getJIndex()) { + return 1; + } + + if(arg0.getJIndex() > arg1.getJIndex()) { + return -1; + } + + return 0; + } + }); + + parentPopulation.clear(); + for(int l=0; l<10 && l2 && jIndex < 0.6) { + break; + } + + System.out.println("\nOne of best param: " + childPopulation.get(l).getParams().toString()); + System.out.println("JIndex: " + jIndex + "\n"); + + pw.println("\nOne of best param: " + childPopulation.get(l).getParams().toString()); + pw.println("JIndex: " + jIndex + "\n"); + pw.flush(); + + parentPopulation.add(childPopulation.get(l)); + } + childPopulation.clear(); + } + + for(int j=0; j bestJIndex) { + bestJIndex = hsvParam.getJIndex(); + bestParam = hsvParam; + } + } + + + if(bestParam != null) { + for(File inputImgFile: inputImgFiles) { + File outputImgFile = new File(outputDir, changeExtension("binarized_" + inputImgFile.getName(), "png")); + + Mat input = Imgcodecs.imread(inputImgFile.getCanonicalPath()); + + HSVBinarization hsv = new HSVBinarization(bestParam.getParams()); + Mat binarized = hsv.apply(input); + + Imgcodecs.imwrite(outputImgFile.getCanonicalPath(), binarized); + //new Display().setImage(binarized); + + input.release(); + binarized.release(); + + System.out.println("Result best param: " + bestParam.toString()); + pw.println("Result best param: " + bestParam.toString()); + pw.flush(); + } + } + + pw.close(); + + } + + + public static List generateRandomPopulation(int populationSize, int paramDimention) { + List randomPopulation = new LinkedList(); + + for(int i=0; i> parameters = new LinkedList>(); + + for(int j=0; j(target, range)); + } + + randomPopulation.add(new HSVParameters(parameters)); + } + + return randomPopulation; + } + + + public static List getCommonPotatoesHSVPreset() { + List presetPopulation = new LinkedList(); + + List> parameters = new LinkedList>(); + + Scalar target3 = new Scalar(1, 241, 73); + Scalar range3 = new Scalar(56, 208, 197); + parameters.add(new Pair(target3, range3)); + + Scalar target4 = new Scalar(171, 81, 144); + Scalar range4 = new Scalar(20, 43, 108); + parameters.add(new Pair(target4, range4)); + + + presetPopulation.add(new HSVParameters(parameters)); + + return presetPopulation; + } + + public static List getCommonPotatoesHSVPreset2() { + List presetPopulation = new LinkedList(); + + List> parameters = new LinkedList>(); + + Scalar target1 = new Scalar(161.0, 81.0, 144.0); + Scalar range1 = new Scalar(29.0, 43.0, 108.0); + parameters.add(new Pair(target1, range1)); + + Scalar target2 = new Scalar(161.0, 72.0, 142.0); + Scalar range2 = new Scalar(27.0, 38.0, 109.0); + parameters.add(new Pair(target2, range2)); + + Scalar target3 = new Scalar(15.0, 138.0, 207.0); + Scalar range3 = new Scalar(41.0, 94.0, 144.0); + parameters.add(new Pair(target3, range3)); + + Scalar target4 = new Scalar(161.0, 72.0, 142.0); + Scalar range4 = new Scalar(27.0, 38.0, 109.0); + parameters.add(new Pair(target4, range4)); + + Scalar target5 = new Scalar(37.0, 231.0, 41.0); + Scalar range5 = new Scalar(51.0, 198.0, 145.0); + parameters.add(new Pair(target5, range5)); + + + presetPopulation.add(new HSVParameters(parameters)); + + return presetPopulation; + } + + + + + public static List getWhitePotatoesHSVPreset() { + List presetPopulation = new LinkedList(); + + List> parameters = new LinkedList>(); + + Scalar target1 = new Scalar(14.0, 139.0, 211.0); + Scalar range1 = new Scalar(41.0, 90.0, 147.0); + parameters.add(new Pair(target1, range1)); + + Scalar target2 = new Scalar(12.0, 134.0, 201.0); + Scalar range2 = new Scalar(37.0, 94.0, 142.0); + parameters.add(new Pair(target2, range2)); + + Scalar target3 = new Scalar(18.0, 138.0, 204.0); + Scalar range3 = new Scalar(46.0, 91.0, 149.0); + parameters.add(new Pair(target3, range3)); + + Scalar target4 = new Scalar(13.0, 133.0, 207.0); + Scalar range4 = new Scalar(42.0, 89.0, 141.0); + parameters.add(new Pair(target4, range4)); + + Scalar target5 = new Scalar(42.0, 238.0, 50.0); + Scalar range5 = new Scalar(54.0, 199.0, 153.0); + parameters.add(new Pair(target5, range5)); + + presetPopulation.add(new HSVParameters(parameters)); + + return presetPopulation; + } + + + public static List getWhitePotatoesHSVPreset2() { + List presetPopulation = new LinkedList(); + + List> parameters = new LinkedList>(); + + Scalar target1 = new Scalar(99, 130, 150); + Scalar range1 = new Scalar(188, 74, 232); + parameters.add(new Pair(target1, range1)); + + presetPopulation.add(new HSVParameters(parameters)); + + return presetPopulation; + } + + + + + public static List getDarkPotatoesHSVPreset() { + List presetPopulation = new LinkedList(); + + List> parameters = new LinkedList>(); + + Scalar target1 = new Scalar(13.0, 192.0, 27.0); + Scalar range1 = new Scalar(241.0, 158.0, 141.0); + parameters.add(new Pair(target1, range1)); + + Scalar target2 = new Scalar(170.0, 35.0, 44.0); + Scalar range2 = new Scalar(40.0, 50.0, 30.0); + parameters.add(new Pair(target2, range2)); + + presetPopulation.add(new HSVParameters(parameters)); + + return presetPopulation; + } + + public static List getDarkPotatoesHSVPreset2() { + List presetPopulation = new LinkedList(); + + List> parameters = new LinkedList>(); + + // fiolet + Scalar target1 = new Scalar(170, 75, 160); + Scalar range1 = new Scalar(25, 40, 100); + parameters.add(new Pair(target1, range1)); + + // yellow + Scalar target2 = new Scalar(10, 87, 200); + Scalar range2 = new Scalar(10, 50, 150); + parameters.add(new Pair(target2, range2)); + + presetPopulation.add(new HSVParameters(parameters)); + + return presetPopulation; + } + + + + public static List newGeneration(List generation, int increment) { + List newGeneration = new LinkedList(); + + Random generator = new Random(); + for(int i=0; i> params = new LinkedList>(); + + List> prt1Params = parent1.getParams(); + List> prt2Params = parent2.getParams(); + + int prt1ParamsSize = prt1Params.size(); + int prt2ParamsSize = prt2Params.size(); + + int childParamsSize = Integer.max(prt1ParamsSize, prt2ParamsSize); + + Random generator = new Random(); + for(int i=0; i> childParameters = new LinkedList>(); + for(Pair param : parent.getParams()) { + Scalar parentTarget = param.getLeft(); + Scalar parentRange = param.getRight(); + + int mutatedH = (int)(parentTarget.val[0]+ThreadLocalRandom.current().nextInt(0, mutationSize) - mutationSize/2); + int mutatedS = (int)(parentTarget.val[1]+ThreadLocalRandom.current().nextInt(0, mutationSize) - mutationSize/2); + int mutatedV = (int)(parentTarget.val[2]+ThreadLocalRandom.current().nextInt(0, mutationSize) - mutationSize/2); + + int mutatedDivH = (int)(parentRange.val[0]+ThreadLocalRandom.current().nextInt(0, mutationSize) - mutationSize/2); + int mutatedDivS = (int)(parentRange.val[1]+ThreadLocalRandom.current().nextInt(0, mutationSize) - mutationSize/2); + int mutatedDivV = (int)(parentRange.val[2]+ThreadLocalRandom.current().nextInt(0, mutationSize) - mutationSize/2); + + // проврка что > 0 && < 255 + mutatedH = mutatedH < 0 ? 0 : (mutatedH > 255 ? 255 : mutatedH); + mutatedS = mutatedS < 0 ? 0 : (mutatedS > 255 ? 255 : mutatedS); + mutatedV = mutatedV < 0 ? 0 : (mutatedV > 255 ? 255 : mutatedV); + + mutatedDivH = mutatedDivH < 0 ? 0 : (mutatedDivH > 255 ? 255 : mutatedDivH); + mutatedDivS = mutatedDivS < 0 ? 0 : (mutatedDivS > 255 ? 255 : mutatedDivS); + mutatedDivV = mutatedDivV < 0 ? 0 : (mutatedDivV > 255 ? 255 : mutatedDivV); + + Scalar childTarget = new Scalar(mutatedH, mutatedS, mutatedV); + Scalar childRange = new Scalar(mutatedDivH, mutatedDivS, mutatedDivV); + + + childParameters.add(new Pair(childTarget, childRange)); + } + + return new HSVParameters(childParameters); + } + + + +} diff --git a/seed-counter-desktop/src/main/java/ru/delkom07/util/xml/JElement.java b/seed-counter-desktop/src/main/java/ru/delkom07/util/xml/JElement.java new file mode 100644 index 0000000..c6038fa --- /dev/null +++ b/seed-counter-desktop/src/main/java/ru/delkom07/util/xml/JElement.java @@ -0,0 +1,866 @@ +package ru.delkom07.util.xml; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Attr; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.TypeInfo; +import org.w3c.dom.UserDataHandler; + +/** + * Класс-обертка на манер JQuery. + * Класс предоставляет удобные методы для работы с элементами DOM дерева. + * В JElement переработаны ВСЕ методы класса Element - в разделе "Переработанные делегированные методы". + * Так же реализованы дополнительные методы для работы с элементами дерева. + * Методы отмечены: + * "=" - абсолютные аналоги методам Element (ничем не отличаются). + * "+" - аналоги методам Element, при этом они возвращают JElement или List вместо Node, NodeList и т.д.. + * "+ Semantically changed" - аналоги методам Element сематнически реализованные иначе. + * "+ from " - аналоги методам Element с измененным названием в связи с измененной семантикой. + * Таким образом данный класс реализует и расширяет полный функционал Element, Node, и частично другие... + * @author Komyshev + * Документация для некоторых методов в конце незакончена. Методы выборки по паттерну не умеют работать с + * несколькими аттрибутами. + * Library class v.0.9 (Development(С). ) + */ +public class JElement { + private Element element = null; + + // -- Обервывание -- + /** + * Конструктор + * Обертывает элемент класса org.w3c.dom.Element. + * @param element + */ + public JElement(Element element) { + this.element = element; + } + + /** + * Конструктор. + * Создает новый Element обернутый в JElement. + * @param doc элемент документа. + * @param tagName имя элемента. + */ + public JElement(Document doc, String tagName) { + this.element = doc.createElement(tagName); + } + + /** + * Вернуть обертываемый элемент. + * @return - обертываемый элемент. + */ + public Element getElement() { + return element; + } + + + // -- Методы расширяющие функционал -- + /** ++ + * Получить первый дочерний элемент. + * @return - первый дочерний элемент если он существует, иначе null. + */ + public JElement get() { + NodeList nodeList = element.getChildNodes(); + for(int i=0; i getAll(String pattern) { + List list = new ArrayList(); + + NodeList nodeList = element.getChildNodes(); + for(int i=0; i params = parsePattern(pattern); + + String tagName = params.get("tagName"); + NodeList nodeList = element.getElementsByTagName(tagName); + for(int i=0; i getAllGlob(String pattern) { + List list = new ArrayList(); + Map params = parsePattern(pattern); + + NodeList nodeList = element.getElementsByTagName(params.get("tagName")); + for(int i=0; i param = parsePattern(pattern); + return matched(el, param); + } + + /** + * Соответствует ли элемент заданному паттерну. + * @param el элемент. + * @param params карта параметров. + * @return true если соответствует, false иначе. + */ + private boolean matched(Element el, Map params) { + // Проверим имя + String tagName = params.get("tagName"); + if(null != tagName) + if(!el.getNodeName().equals(tagName)) + return false; // Без разговоров + + // Проверим id если указан + String id = params.get("id"); + if(null != id) + if(!el.getAttribute("id").equals(id)) + return false; // Тоже дальше не заморачиваемся + + // Классов у элемента может быть много, надо проверить все + String class_ = params.get("class"); + if(null != class_) { + boolean matchClass = false; // может совпадет кто нибудь + String[] array = el.getAttribute("class").split(" "); + for(int i=0; i parsePattern(String pattern) { + //pattern.matches("\\w+[#\\w+]?[\\.\\w+]?[&\\w]?"); + Map map = new HashMap(); + if(""==pattern) return map; + + String subStr = ""; int i; + for(i=1; i subMap = parsePattern(pattern.substring(i)); + map.putAll(subMap); + break; + } + } + if(subStr.equals("")) + subStr = pattern.substring(0, i); + + char ch0 = pattern.charAt(0); + if('#'==ch0) { + map.put("id", subStr.substring(1)); + } else + if('.'==ch0) { + map.put("class", subStr.substring(1)); + } else + if('&'==ch0) { + String[] arr = subStr.substring(1).split("="); + map.put("attr", arr[0]); + map.put("value", arr[1]); + } else { + map.put("tagName", subStr); + } + + return map; + } + + + // -- Переработанные делегированные методы -- + /* Должны соответствовать всем методам класса Element. + * При этом некоторые методы принимающие параметры, раздваиваются в данном классе + * т.к необходимо чтобы методы работали как для Element, так и для JElement.*/ + /** + + * Добавить дочерний элемент в текущий. + * @param newChild - добавляемый элемент. + * @return - текущий элемент с добавленным элементом. + * @throws DOMException + */ + public JElement appendChild(JElement newChild) throws DOMException { + return new JElement((Element) element.appendChild(newChild.getElement())); + } + public JElement appendChild(Element newChild) throws DOMException { + return new JElement((Element) element.appendChild(newChild)); + } + + /** + + * Клонировать элемент. + * @param deep - клонировать ли потомков. + * @return - клонированный элемент. + */ + public JElement cloneJElement(boolean deep) { + return new JElement((Element) element.cloneNode(deep)); + } + + /** = + * Сравнить позицию элемента в документе. + * @return - позиция элемента в документе относительно другого. + */ + public short compareDocumentPosition(JElement other) throws DOMException { + return element.compareDocumentPosition(other.getElement()); + } + public short compareDocumentPosition(Element other) throws DOMException { + return element.compareDocumentPosition(other); + } + + /** + Semantically changed + * Получить все аттрибуты элемента. + * @return все аттрибуты элемента в виде карты. + */ + public Map getAttributes() { + Map map = new HashMap(); + + NamedNodeMap nodeMap = element.getAttributes(); + for(int i=0; i getChildJElements() { + LinkedList list = new LinkedList(); + + NodeList nodeList = element.getChildNodes(); + for(int i=0; i=0; i--) { + Node node = nodeList.item(i); + if(Node.ELEMENT_NODE == node.getNodeType()) + return new JElement( (Element) node ); + } + return null; // Если не нашел + } + + /** = + * Получить локальное имя элемента. + * @return локальное имя элемента. + */ + public String getLocalName() { + return element.getLocalName(); + } + + /** = + * Получить URI пространства имен. + * @return URI пространства имен. + */ + public String getNamespaceURI() { + return element.getNamespaceURI(); + } + + /** + + * Получить следующий равноуровневый элемент. + * @return следующий равноуровневый элемент. + */ + public JElement getNextSibling() { + Node node = element.getNextSibling(); + if(null == node) return null; + + while(Node.ELEMENT_NODE != node.getNodeType()) { + node = node.getNextSibling(); + if(null == node) return null; + } + + return new JElement( (Element) node ); + } + + /** + from getNodeName() + * Получить название элемента. + * @return название элемента. + */ + public String getJElementName() { + return element.getNodeName(); + } + public String getName() { + return element.getNodeName(); + } + + /** + Not userful. Return Node.ELEMENT_NODE always. + * Получить тип нода. + * @return тип нода. + */ + public short getNodeType() { + return element.getNodeType(); + } + + /** + from getNodeValue() + * Получить значение элемента. + * @return значение элемента. + * @throws DOMException + */ + public String getJElementValue() throws DOMException { + return element.getNodeValue(); + } + public String getValue() { + return element.getNodeValue(); + } + + /** = + * Получить докумет-владельца. + * @return докумет-владелец. + */ + public Document getOwnerDocument() { + return element.getOwnerDocument(); + } + + /** + from getParentNode() + * Получить родительский элемент. + * @return родительский элемент. + */ + public JElement getParentJElement() { + /* По сути - родитель должен быть типа Element, так как мы не используем всякие там + * аттрибуты в качестве JElement... не так ли ? */ + return new JElement( (Element) element.getParentNode() ); + } + + /** = + * Получить префикс. + * @return префикс. + */ + public String getPrefix() { + return element.getPrefix(); + } + + /** + + * Получить предидующий равноуровневый элемент. + * @return предидующий равноуровневый элемент. + */ + public JElement getPreviousSibling() { + Node node = element.getPreviousSibling(); + if(null == node) return null; + + while(Node.ELEMENT_NODE != node.getNodeType()) { + node = node.getPreviousSibling(); + if(null == node) return null; + } + + return new JElement( (Element) node ); + } + + /** = + * Получить текстовое содержание. + * @return текстовое содержание. + * @throws DOMException + */ + public String getTextContent() throws DOMException { + return element.getTextContent(); + } + + /** = + * Получить пользовательские данные. + * @param key ключ ассоциированный с данными. + * @return пользовательские данные. + */ + public Object getUserData(String key) { + return element.getUserData(key); + } + + /** = + * Есть ли аттрибуты у элемента. + * @return true если есть, false иначе. + */ + public boolean hasAttributes() { + return element.hasAttributes(); + } + + /** + Semantically changed from hasChildNodes() + * Есть ли дочерние элементы. + * @return true если есть, false иначе. + */ + public boolean hasChildElements() { + NodeList nodeList = element.getChildNodes(); + + for(int i=0; i getJElementsByTagName(String name) { + List list = new LinkedList(); + + NodeList nodeList = element.getElementsByTagName(name); + for(int i=0; i getJElementsByTagNameNS(String name, String namespace) throws DOMException { + LinkedList list = new LinkedList(); + + NodeList nodeList = element.getElementsByTagNameNS(name, namespace); + for(int i=0; i(result, transformationDeviance); // Edited + } + + // a wrapper for the getTransformationDeviation method in AbstractOLS class + public double getTransformationDeviation(RegressionModel model, ColorSpace featuresSpace) throws IllegalStateException { + List train = new ArrayList<>(); + List answers = new ArrayList<>(); + + calculateTrainAndAnswers(featuresSpace, featuresSpace, train, answers); + + double deviation; + + try { + deviation = model.getTransformationDeviance(train, answers); + } catch (SingularMatrixException e) { + throw new IllegalStateException("Couldn't calculate the transformation matrix given this reference"); + } + + return deviation; + } + + private void calculateTrainAndAnswers(ColorSpace featuresSpace, ColorSpace targetSpace, + List train, List answers) { + for (Integer row = 0; row < rowCount(); ++row) { + for (Integer col = 0; col < colCount(); ++col) { + List samplePoints = getSamplePoints(row, col); + for (DoubleBuffer s : samplePoints) { + train.add(featuresSpace.convertFromBGR(s, false)); + DoubleBuffer referenceColor = DoubleBuffer.wrap(BGR_REFERENCE_COLORS.get(row).get(col).val); + answers.add(targetSpace.convertFromBGR(referenceColor, false)); + } + } + } + } + + public double labDeviationFromReference() { + return getCellColors(checkerImage, false).calculateMetric(new EuclideanLab()); + } + + public CellColors getCellColors(Mat checkerImage) { + return getCellColors(checkerImage, true); + } + + public CellColors getCellColors(Mat checkerImage, boolean allPoints) { + CellColors cellColors = new CellColors(); + + for (Integer row = 0; row < rowCount(); ++row) { + for (Integer col = 0; col < colCount(); ++col) { + List actualColors = getSamplePoints(checkerImage, row, col, allPoints); + DoubleBuffer referenceColor = DoubleBuffer.wrap(BGR_REFERENCE_COLORS.get(row).get(col).val); + for (DoubleBuffer color : actualColors) { + cellColors.addColor(new Color(color), new Color(referenceColor)); + } + } + } + + return cellColors; + } + + private List getSamplePoints(Integer row, Integer col) { + return getSamplePoints(checkerImage, row, col, false); + } + + private List getSamplePoints(Mat checkerImage, Integer row, Integer col, boolean allPoints) { + final int STEP = 10; + final int CHANNELS = 3; + + Point center = centers.get(row).get(col); + List surroundingPoints = getSurroundingPoints(center); + + List points = new ArrayList<>(); + double[] result; + + if (allPoints) { + int minX = (int) surroundingPoints.get(0).x; + int minY = (int) surroundingPoints.get(0).y; + int maxX = (int) surroundingPoints.get(8).x; + int maxY = (int) surroundingPoints.get(8).y; + int xSize = (maxX - minX) / STEP + 1; + int ySize = (maxY - minY) / STEP + 1; + result = new double[xSize * ySize * CHANNELS]; + int index = 0; + + for (int y = minY; y <= maxY; y += STEP) { + for (int x = minX; x <= maxX; x += STEP) { + double[] color = checkerImage.get(y, x); + System.arraycopy(color, 0, result, index, CHANNELS); + points.add(DoubleBuffer.wrap(result, index, CHANNELS)); + index += CHANNELS; + } + } + } else { + result = new double[surroundingPoints.size() * CHANNELS]; + for (int i = 0; i < surroundingPoints.size(); ++i) { + Point p = surroundingPoints.get(i); + double[] color = checkerImage.get((int) p.y, (int) p.x); + System.arraycopy(color, 0, result, i * 3, CHANNELS); + points.add(DoubleBuffer.wrap(result, i*CHANNELS, CHANNELS)); + } + } + + return points; + } + + public Mat drawSamplePoints() { + Mat result = checkerImage.clone(); + Scalar red = new Scalar(0, 0, 255); + Scalar blue = new Scalar(255, 0, 0); + + for (Integer row = 0; row < rowCount(); ++row) { + for (Integer col = 0; col < colCount(); ++col) { + Point center = centers.get(row).get(col); + List points = getSurroundingPoints(center); + int i = 0; + for (Point p : points) { + if (i % 2 == 0) { + Imgproc.circle(result, p, 10, red, Core.FILLED); + } else { + Imgproc.circle(result, p, 10, blue, Core.FILLED); + } + i += 1; + } + } + } + + return result; + } + + private Point correctByReference(Point center, int row, int col) { + final int ITERATIONS = 3; + final double STEP_CHANGE = 1.2; + final double THRESHOLD = (row <= 1 && col == 3 ? 2.0 : 1.1); + final double VARIANCE_THRESHOLD = 100.0; + final double DISTANCE_COEFFICIENT = 1.5; + + return correctByReference(center, row, col, ITERATIONS, STEP_CHANGE, THRESHOLD, + VARIANCE_THRESHOLD, DISTANCE_COEFFICIENT); + } + + private Point correctByReference(Point center, int row, int col, int iterations, double stepChange, + double threshold, double varianceThreshold, double distanceCoefficient) { + final double INFINITY = 1e9; + + Color referenceColor = new Color(DoubleBuffer.wrap(BGR_REFERENCE_COLORS.get(row).get(col).val)); + EuclideanLab metric = new EuclideanLab(); + double xStep = xScale; + double yStep = yScale; + + for (int iteration = 0; iteration < iterations; ++iteration) { + List points = getSurroundingPoints(center); + int nearestPoint = -1; + double nearestDistance = INFINITY; + + for (int i = 0; i < points.size(); ++i) { + Point point = points.get(i); + int x = (int) (center.x + (point.x - center.x) * distanceCoefficient); + int y = (int) (center.y + (point.y - center.y) * distanceCoefficient); + if (getValueVariance(x, y) < varianceThreshold) { + Color color = getMeanColor(x, y); + double distance = metric.calculate(color, referenceColor); + if (distance < nearestDistance) { + nearestPoint = i; + nearestDistance = distance; + } + } + } + + if (nearestPoint == -1) { + return center; + } + Point point = points.get(8 - nearestPoint); + int x = (int) (center.x + (point.x - center.x)); + int y = (int) (center.y + (point.y - center.y)); + Color color = getMeanColor(x, y); + double oppositeDistance = metric.calculate(color, referenceColor); + + if (oppositeDistance < nearestDistance * threshold) { + return center; + } + + if (RIGHT_INDEXES.contains(nearestPoint)) { + if (checkCorrectness(points, xStep, 0.0)) { + center.x += xStep; + } + xStep /= stepChange; + } else if (LEFT_INDEXES.contains(nearestPoint)) { + if (checkCorrectness(points, -xStep, 0.0)) { + center.x -= xStep; + } + xStep /= stepChange; + } + + if (BOTTOM_INDEXES.contains(nearestPoint)) { + if (checkCorrectness(points, 0.0, yStep)) { + center.y += yStep; + } + yStep /= stepChange; + } else if (TOP_INDEXES.contains(nearestPoint)) { + if (checkCorrectness(points, 0.0, -yStep)) { + center.y -= yStep; + } + yStep /= stepChange; + } + } + + return center; + } + + private Color getMeanColor(int x, int y) { + double[] result = {0.0, 0.0, 0.0}; + + for (int row = -1; row < 2; ++row) { + for (int col = -1; col < 2; ++col) { + double[] color = getColor(x + row * xColorPatchSize, y + col * yColorPatchSize); + for (int i = 0; i < 3; ++i) { + result[i] += color[i]; + } + } + } + + for (int i = 0; i < 3; ++i) { + result[i] /= 9; + } + + return new Color(DoubleBuffer.wrap(result)); + } + + private double getValueVariance(int x, int y) { + double firstMoment = 0.0; + double secondMoment = 0.0; + + for (int row = -1; row < 2; ++row) { + for (int col = -1; col < 2; ++col) { + double[] color = getColor(x + row * xColorPatchSize, y + col * yColorPatchSize); + double value = Math.max(color[0], Math.max(color[1], color[2])); + firstMoment += value; + secondMoment += Math.pow(value, 2.0); + } + } + + firstMoment /= 9.0; + secondMoment /= 9.0; + + return secondMoment - Math.pow(firstMoment, 2.0); + } + + private double[] getColor(int x, int y) { + if (x < 0) { + x = 0; + } else if (x >= checkerImage.cols()) { + x = checkerImage.cols() - 1; + } + if (y < 0) { + y = 0; + } else if (y >= checkerImage.rows()) { + y = checkerImage.rows() - 1; + } + + return checkerImage.get(y, x); + } + + private Point correctByDeviation(Point center) { + final int ITERATIONS = 10; + final double THRESHOLD = 1.5; + final double STEP_CHANGE = 1.5; + + return correctByDeviation(center, ITERATIONS, THRESHOLD, STEP_CHANGE); + } + + private Point correctByDeviation(Point center, int iterations, double threshold, double stepChange) { + double xStep = xScale; + double yStep = yScale; + + for (int iteration = 0; iteration < iterations; ++iteration) { + List points = getSurroundingPoints(center); + double top = deviationSum(points, center, TOP_INDEXES); + double bottom = deviationSum(points, center, BOTTOM_INDEXES); + double left = deviationSum(points, center, LEFT_INDEXES); + double right = deviationSum(points, center, RIGHT_INDEXES); + Point newCenter = center.clone(); + + if (left >= right * threshold) { + if (checkCorrectness(points, xStep, 0.0)) { + double newLeft = deviationSum(points, center, LEFT_INDEXES, xStep, 0.0); + double newRight = deviationSum(points, center, RIGHT_INDEXES, xStep, 0.0); + if (newLeft + newRight < left + right) { + newCenter.x += xStep; + } + } + xStep /= stepChange; + } else if (right >= left * threshold) { + if (checkCorrectness(points, -xStep, 0.0)) { + double newLeft = deviationSum(points, center, LEFT_INDEXES, -xStep, 0.0); + double newRight = deviationSum(points, center, RIGHT_INDEXES, -xStep, 0.0); + if (newLeft + newRight < left + right) { + newCenter.x -= xStep; + } + } + xStep /= stepChange; + } + + if (top >= bottom * threshold) { + if (checkCorrectness(points, 0.0, yStep)) { + double newTop = deviationSum(points, center, TOP_INDEXES, 0.0, yStep); + double newBottom = deviationSum(points, center, BOTTOM_INDEXES, 0.0, yStep); + if (newTop + newBottom < top + bottom) { + newCenter.y += yStep; + } + } + yStep /= stepChange; + } else if (bottom >= top * threshold) { + if (checkCorrectness(points, 0.0, -yStep)) { + double newTop = deviationSum(points, center, TOP_INDEXES, 0.0, -yStep); + double newBottom = deviationSum(points, center, BOTTOM_INDEXES, 0.0, -yStep); + if (newTop + newBottom < top + bottom) { + newCenter.y -= yStep; + } + } + yStep /= stepChange; + } + + center = newCenter; + } + + return center; + } + + private double deviationSum(List points, Point center, List indexes) { + return deviationSum(points, center, indexes, 0.0, 0.0); + } + + private double deviationSum(List points, Point center, List indexes, double xStep, double yStep) { + Color centerColor = new Color(DoubleBuffer.wrap(checkerImage.get((int) (center.y + yStep), + (int) (center.x + xStep)))); + EuclideanRGB metric = new EuclideanRGB(); + + return indexes.stream().map(x -> { + Point point = points.get(x); + Color color = new Color(DoubleBuffer.wrap( + checkerImage.get((int) (point.y + yStep), (int) (point.x + xStep)))); + return metric.calculate(centerColor, color); + }).reduce(0.0, (x,y) -> x + y); + } + + private boolean checkCorrectness(List points, double xStep, double yStep) { + for (Point p : points) { + if (p.x + xStep < 0 || p.x + xStep >= checkerImage.cols() + || p.y + yStep < 0 || p.y + yStep >= checkerImage.rows()) { + return false; + } + } + + return true; + } + + private List getSurroundingPoints(Point center) { + return Arrays.asList( + new Point(center.x - xScale, center.y - yScale), + new Point(center.x, center.y - yScale), + new Point(center.x + xScale, center.y - yScale), + new Point(center.x - xScale, center.y), + new Point(center.x, center.y), + new Point(center.x + xScale, center.y), + new Point(center.x - xScale, center.y + yScale), + new Point(center.x, center.y + yScale), + new Point(center.x + xScale, center.y + yScale) + ); + } + + private int rowCount() { + return BGR_REFERENCE_COLORS.size(); + } + + private int colCount() { + return BGR_REFERENCE_COLORS.get(0).size(); + } + + + // Edited: + public void colorPrint(Mat img) { + float persent = 0.3f; + + MatOfPoint matOfPoint = new MatOfPoint(quad.tl(), quad.tr(), quad.br(), quad.bl()); + + Point[] tlbr = SpecificGeometry.getTopLeftAndBottomRight(matOfPoint.toList()); + Point tl = tlbr[0]; + Point br = tlbr[1]; + Mat binMark = Mat.zeros((int)(br.y - tl.y), (int)(br.x - tl.x), CvType.CV_8SC1); + + MatOfPoint matOfPoint2 = new MatOfPoint( + new Point(quad.tl().x - tl.x, quad.tl().y - tl.y), + new Point(quad.tr().x - tl.x, quad.tr().y - tl.y), + new Point(quad.br().x - tl.x, quad.br().y - tl.y), + new Point(quad.bl().x - tl.x, quad.bl().y - tl.y) + ); + Imgproc.fillConvexPoly(binMark, matOfPoint2, new Scalar(255)); + + for(int i=(int)tl.y; i quad1 = getQuad(getHomography(image), scale); + if (!quad1.isPresent()) { + return fullImageQuad(image); + } + + image = imageSplice(image, quad1.get()); + Optional quad2 = getQuad(getHomography(image), 0.0); + image.release(); + if (!quad2.isPresent()) { + return fullImageQuad(image); + } + + return shiftQuad(quad2.get(), quad1.get()); + } + + private Quad fullImageQuad(Mat image) { + return new Quad( + new Point(0.0, 0.0), + new Point(image.cols() - 1, 0.0), + new Point(image.cols() - 1, image.rows() - 1), + new Point(0.0, image.rows() - 1) + ); + } + + private Optional getHomography(Mat image) { + MatOfKeyPoint keypoints = new MatOfKeyPoint(); + if (image.rows() < 100 || image.cols() < 50) { + return Optional.empty(); + } + detector.detect(image, keypoints); + if ((int) keypoints.size().width * (int) keypoints.size().height < 2) { + return Optional.empty(); + } + + MatOfKeyPoint descriptors = new MatOfKeyPoint(); + extractor.compute(image, keypoints, descriptors); + + LinkedList goodMatches = getGoodMatches(descriptors); + + if (goodMatches.isEmpty()) { + return Optional.empty(); + } + + return getHomography(keypoints, goodMatches); + } + + private LinkedList getGoodMatches(MatOfKeyPoint descriptors) { + List matches = new LinkedList<>(); + DescriptorMatcher descriptorMatcher = DescriptorMatcher.create( + matchingModel.getMatcher()); + + synchronized (FindColorChecker.class) { + descriptorMatcher.knnMatch(referenceDescriptors, descriptors, matches, 4); + } + + + LinkedList goodMatches = new LinkedList<>(); + + for (MatOfDMatch matofDMatch : matches) { + DMatch[] dmatcharray = matofDMatch.toArray(); + DMatch m1 = dmatcharray[0]; + DMatch m2 = dmatcharray[1]; + + if (m1.distance <= m2.distance * matchingModel.getThreshold()) { + goodMatches.addLast(m1); + } + } + + return goodMatches; + } + + private Optional getHomography(MatOfKeyPoint keypoints, LinkedList goodMatches) { + List referenceKeypointlist = referenceKeypoints.toList(); + List keypointlist = keypoints.toList(); + + LinkedList referencePoints = new LinkedList<>(); + LinkedList points = new LinkedList<>(); + + for (DMatch goodMatch : goodMatches) { + referencePoints.addLast(referenceKeypointlist.get(goodMatch.queryIdx).pt); + points.addLast(keypointlist.get(goodMatch.trainIdx).pt); + } + + MatOfPoint2f referenceMatOfPoint2f = new MatOfPoint2f(); + referenceMatOfPoint2f.fromList(referencePoints); + MatOfPoint2f matOfPoint2f = new MatOfPoint2f(); + matOfPoint2f.fromList(points); + + return Optional.of(Calib3d.findHomography( + referenceMatOfPoint2f, matOfPoint2f, Calib3d.RANSAC, 3)) + .filter(x -> x.cols() > 0 && x.rows() > 0); + } + + private Optional getQuad(Optional homography, double scale) { + if (!homography.isPresent()) { + return Optional.empty(); + } + + Mat corners = new Mat(4, 1, CvType.CV_32FC2); + Mat referenceCorners = new Mat(4, 1, CvType.CV_32FC2); + + referenceCorners.put(0, 0, -scale * referenceImage.cols(), -scale * referenceImage.rows()); + referenceCorners.put(1, 0, (1.0 + scale) * referenceImage.cols(), -scale * referenceImage.rows()); + referenceCorners.put(2, 0, (1.0 + scale) * referenceImage.cols(), (1.0 + scale) * referenceImage.rows()); + referenceCorners.put(3, 0, -scale * referenceImage.cols(), (1.0 + scale) * referenceImage.rows()); + + Core.perspectiveTransform(referenceCorners, corners, homography.get()); + + return Optional.of(new Quad(new Point(corners.get(0, 0)),new Point(corners.get(1, 0)), + new Point(corners.get(2, 0)), new Point(corners.get(3, 0)))); + } + + public void fillColorChecker(Mat image, Quad quad) { + MatOfPoint points = new MatOfPoint(); + + points.fromArray(quad.getPoints()); + Imgproc.fillConvexPoly(image, points, getBackgroundColor(image, quad)); + } + + private Mat imageSplice(Mat image, Quad quad) { + Range rows = new Range(clipRow(top(quad), image), clipRow(bottom(quad), image) + 1); + Range cols = new Range(clipCol(left(quad), image), clipCol(right(quad), image) + 1); + + return new Mat(image, rows, cols); + } + + private int clipRow(int row, Mat image) { + return Math.max(0, Math.min(image.rows() - 1, row)); + } + + private int clipCol(int col, Mat image) { + return Math.max(0, Math.min(image.cols() - 1, col)); + } + + private Quad shiftQuad(Quad quad, Quad shift) { + return new Quad( + shiftPoint(quad.tl(), shift), + shiftPoint(quad.tr(), shift), + shiftPoint(quad.br(), shift), + shiftPoint(quad.bl(), shift) + ); + } + + private Point shiftPoint(Point point, Quad shift) { + return new Point(point.x + left(shift), point.y + top(shift)); + } + + private int left(Quad quad) { + return (int) Math.min( + Math.min(quad.tl().x, quad.tr().x), + Math.min(quad.bl().x, quad.br().x) + ); + } + + private int right(Quad quad) { + return (int) Math.max( + Math.max(quad.tl().x, quad.tr().x), + Math.max(quad.bl().x, quad.br().x) + ); + } + + private int top(Quad quad) { + return (int) Math.min( + Math.min(quad.tl().y, quad.tr().y), + Math.min(quad.bl().y, quad.br().y) + ); + } + + private int bottom(Quad quad) { + return (int) Math.max( + Math.max(quad.tl().y, quad.tr().y), + Math.max(quad.bl().y, quad.br().y) + ); + } + + private Scalar getBackgroundColor(Mat image, Quad quad) { + List colors = new ArrayList<>(); + double sumBlue = 0.0; + double sumGreen = 0.0; + double sumRed = 0.0; + + while (colors.size() < 1000) { + double x = ThreadLocalRandom.current().nextDouble(0.0, image.cols()); + double y = ThreadLocalRandom.current().nextDouble(0.0, image.rows()); + + if (image.get((int) x, (int) y) != null && !quad.isInside(new Point(x, y))) { + colors.add(image.get((int) x, (int) y)); + sumBlue += colors.get(colors.size() - 1)[0]; + sumGreen += colors.get(colors.size() - 1)[1]; + sumRed += colors.get(colors.size() - 1)[2]; + } + } + double meanBlue = sumBlue / colors.size(); + double meanGreen = sumGreen / colors.size(); + double meanRed = sumRed / colors.size(); + + for (int i = 0; i < 10; ++i) { + List buffer = new ArrayList<>(); + sumBlue = 0.0; + sumGreen = 0.0; + sumRed = 0.0; + for (double[] c : colors) { + if (Math.abs(c[0] - meanBlue) <= 25 && + Math.abs(c[1] - meanGreen) <= 25 && + Math.abs(c[2] - meanRed) <= 25) { + sumBlue += c[0]; + sumGreen += c[1]; + sumRed += c[2]; + buffer.add(c); + } + } + colors = buffer; + meanBlue = sumBlue / colors.size(); + meanGreen = sumGreen / colors.size(); + meanRed = sumRed / colors.size(); + } + + return new Scalar(meanBlue, meanGreen, meanRed); + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/colorchecker/MatchingModel.java b/seed-counter-desktop/src/main/java/smirnov/colorchecker/MatchingModel.java new file mode 100644 index 0000000..a6cccb1 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/colorchecker/MatchingModel.java @@ -0,0 +1,34 @@ +package smirnov.colorchecker; + +import org.opencv.features2d.Feature2D; + +public class MatchingModel { + private final Feature2D detector; + private final Feature2D extractor; + private final int matcher; + private final float threshold; + + public MatchingModel(Feature2D detector, Feature2D extractor, + int matcher, float threshold) { + this.detector = detector; + this.extractor = extractor; + this.matcher = matcher; + this.threshold = threshold; + } + + public Feature2D getDetector() { + return detector; + } + + public Feature2D getExtractor() { + return extractor; + } + + public int getMatcher() { + return matcher; + } + + public float getThreshold() { + return threshold; + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/colormetric/CellColors.java b/seed-counter-desktop/src/main/java/smirnov/colormetric/CellColors.java new file mode 100644 index 0000000..77abcdf --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/colormetric/CellColors.java @@ -0,0 +1,27 @@ +package smirnov.colormetric; + +import java.util.ArrayList; +import java.util.List; + +public class CellColors { + private final List referenceColors; + private final List actualColors; + + public CellColors() { + referenceColors = new ArrayList<>(); + actualColors = new ArrayList<>(); + } + + public void addColor(Color actualColor, Color referenceColor) { + actualColors.add(actualColor); + referenceColors.add(referenceColor); + } + + public double calculateMetric(ColorMetric metric) { + double sum = 0.0; + for (int i = 0; i < referenceColors.size(); ++i) { + sum += metric.calculate(actualColors.get(i), referenceColors.get(i)); + } + return sum / referenceColors.size(); + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/colormetric/Color.java b/seed-counter-desktop/src/main/java/smirnov/colormetric/Color.java new file mode 100644 index 0000000..5feddd9 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/colormetric/Color.java @@ -0,0 +1,65 @@ +package smirnov.colormetric; + +import java.nio.DoubleBuffer; + +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; + +public class Color { + private final DoubleBuffer bgr; + private DoubleBuffer lab; + + public Color(DoubleBuffer bgr) { + this.bgr = bgr; + this.lab = null; + } + + public static double channel(DoubleBuffer color, int channel) { + return color.get(color.position() + channel); + } + + public double red() { + return channel(bgr, 2); + } + + public double green() { + return channel(bgr, 1); + } + + public double blue() { + return channel(bgr, 0); + } + + public double lightness() { + calculateLab(); + return channel(lab, 0) / 2.55; + } + + public double a() { + calculateLab(); + return channel(lab, 1) - 128.0; + } + + public double b() { + calculateLab(); + return channel(lab, 2) - 128.0; + } + + private void calculateLab() { + if (this.lab == null) { + Scalar lab = bgrToLabScalar(new Scalar(channel(bgr, 0), channel(bgr, 1), channel(bgr, 2))); + this.lab = DoubleBuffer.wrap(lab.val); + } + } + + private Scalar bgrToLabScalar(Scalar color) { + Mat bgr = new Mat(1, 1, CvType.CV_8UC4, color); + Mat lab = new Mat(1, 1, CvType.CV_32FC3); + Imgproc.cvtColor(bgr, lab, Imgproc.COLOR_BGR2Lab); + + return new Scalar(lab.get(0, 0)); + } + +} diff --git a/seed-counter-desktop/src/main/java/smirnov/colormetric/ColorMetric.java b/seed-counter-desktop/src/main/java/smirnov/colormetric/ColorMetric.java new file mode 100644 index 0000000..6b25881 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/colormetric/ColorMetric.java @@ -0,0 +1,5 @@ +package smirnov.colormetric; + +public interface ColorMetric { + double calculate(Color c1, Color c2); +} diff --git a/seed-counter-desktop/src/main/java/smirnov/colormetric/EuclideanLab.java b/seed-counter-desktop/src/main/java/smirnov/colormetric/EuclideanLab.java new file mode 100644 index 0000000..7aacd92 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/colormetric/EuclideanLab.java @@ -0,0 +1,12 @@ +package smirnov.colormetric; + +public class EuclideanLab implements ColorMetric { + @Override + public double calculate(Color c1, Color c2) { + return Math.sqrt( + Math.pow(c1.lightness() - c2.lightness(), 2.0) + + Math.pow(c1.a() - c2.a(), 2.0) + + Math.pow(c1.b() - c2.b(), 2.0) + ); + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/colormetric/EuclideanRGB.java b/seed-counter-desktop/src/main/java/smirnov/colormetric/EuclideanRGB.java new file mode 100644 index 0000000..7a32567 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/colormetric/EuclideanRGB.java @@ -0,0 +1,12 @@ +package smirnov.colormetric; + +public class EuclideanRGB implements ColorMetric { + @Override + public double calculate(Color c1, Color c2) { + return Math.sqrt( + Math.pow(c1.red() - c2.red(), 2.0) + + Math.pow(c1.green() - c2.green(), 2.0) + + Math.pow(c1.blue() - c2.blue(), 2.0) + ); + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/common/Clusterizer.java b/seed-counter-desktop/src/main/java/smirnov/common/Clusterizer.java new file mode 100644 index 0000000..363e80c --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/common/Clusterizer.java @@ -0,0 +1,119 @@ +package smirnov.common; + +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.TermCriteria; + +public class Clusterizer { + private final int clusters; + + public Clusterizer(int clusters) { + this.clusters = clusters; + } + + /* takes: + * source image points reshaped into (rows*cols, channels, 1), + * cluster labels (rows*cols, 1, 1) + */ + public double getBackgroundVariance(Mat samples, Mat labels) { + List> clusterStatistics = new ArrayList<>(); + for (int i = 0; i < clusters; ++i) { + clusterStatistics.add(Stream.of( + new SimpleEntry<>("second_moment", 0.0), + new SimpleEntry<>("count", 0.0) + ).collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue))); + for (int col = 0; col < samples.cols(); ++col) { + clusterStatistics.get(i).put("sum_channel_" + col, 0.0); + } + } + + for (int row = 0; row < labels.rows(); ++row) { + int label = (int)labels.get(row, 0)[0]; + Map map = clusterStatistics.get(label); + + for (int col = 0; col < samples.cols(); ++col) { + map.put("second_moment", map.get("second_moment") + Math.pow(samples.get(row, col)[0], 2)); + map.put("sum_channel_" + col, map.get("sum_channel_" + col) + samples.get(row, col)[0]); + } + map.put("count", map.get("count") + 1.0); + } + + Map maxCluster = clusterStatistics.get(0); + for (Map c : clusterStatistics) { + if (c.get("count") > maxCluster.get("count")) { + maxCluster = c; + } + } + + double result = maxCluster.get("second_moment") / maxCluster.get("count"); + + for (int col = 0; col < samples.cols(); ++col) { + result -= Math.pow(maxCluster.get("sum_channel_" + col) / maxCluster.get("count"), 2); + } + + return result; + } + + /* takes: + * source image, + * array { + * cluster labels (rows*cols, 1, 1), + * cluster centroids (CLUSTERS, channels, 1) + * } + */ + public Mat getBackgroundSegmentation(Mat image, Mat[] clusters) { + Mat samples = getClusteringSamples(image); + Mat labels = clusters[0]; + Mat centroids = clusters[1]; + + for (int row = 0; row < labels.rows(); ++row) { + int label = (int)labels.get(row, 0)[0]; + Mat samplesRow = samples.row(row); + centroids.row(label).copyTo(samplesRow); + } + labels.release(); + centroids.release(); + + samples = samples.reshape(image.channels(), image.rows()); + samples.convertTo(samples, image.type()); + + return samples; + } + + // returns: source image points reshaped into (rows*cols, channels, 1) + public Mat getClusteringSamples(Mat image) { + Mat samples = image.reshape(1, image.rows() * image.cols()); + samples.convertTo(samples, CvType.CV_32F); + + return samples; + } + + /* takes: source image points reshaped into (rows*cols, channels, 1) + * returns: { + * cluster labels (rows*cols, 1, 1), + * cluster centroids (CLUSTERS, channels, 1) + * } + */ + public Mat[] clusterize(Mat samples) { + return clusterize(samples, 3, 5, 1e-5); + } + + public Mat[] clusterize(Mat samples, int attempts, int maxIterations, double epsilon) { + Mat labels = new Mat(samples.rows(), 1, CvType.CV_8U); + Mat centroids = new Mat(clusters, 1, CvType.CV_32F); + TermCriteria criteria = new TermCriteria(TermCriteria.EPS | TermCriteria.MAX_ITER, + maxIterations, epsilon); + + Core.kmeans(samples, clusters, labels, criteria, attempts, Core.KMEANS_RANDOM_CENTERS, centroids); + + return new Mat[] {labels, centroids}; + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/common/HSVBinarization.java b/seed-counter-desktop/src/main/java/smirnov/common/HSVBinarization.java new file mode 100644 index 0000000..c66e20e --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/common/HSVBinarization.java @@ -0,0 +1,53 @@ +package smirnov.common; + +import java.util.List; + +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; + +import ru.delkom07.util.Pair; + + +public class HSVBinarization { + private final List> targetsAndRanges; + + public HSVBinarization(List> targetsAndRanges) { + this.targetsAndRanges = targetsAndRanges; + } + + + public Mat apply(Mat input) { + int width = input.cols(); + int height = input.rows(); + + Mat hsvImg = new Mat(height, width, CvType.CV_8UC3); + Imgproc.cvtColor(input, hsvImg, Imgproc.COLOR_BGR2HSV); + + Mat output = new Mat(height, width, CvType.CV_8UC1); + for(int i=0; i targetAndRange : targetsAndRanges) { + Scalar target = targetAndRange.getLeft(); + Scalar range = targetAndRange.getRight(); + + if(Math.abs(value[0]-target.val[0])> targetsAndRanges) { + HSVBinarization hsv = new HSVBinarization(targetsAndRanges); + return hsv.apply(image); + } + + public static Mat filterByMask(Mat image, Mat mask) { + List channels = new ArrayList<>(); + Core.split(image, channels); + for (int i = 0; i < 3; ++i) { + Mat c = channels.get(i); + Core.bitwise_and(c, mask, c); + channels.set(i, c); + } + Mat filtered = new Mat(image.rows(), image.cols(), CvType.CV_8UC3); + Core.merge(channels, filtered); + for (Mat c : channels) { + c.release(); + } + + return filtered; + } + + public static List getContours(Mat image) { + Mat gray = new Mat(); + Imgproc.cvtColor(image, gray, Imgproc.COLOR_RGB2GRAY); + List contours = new ArrayList<>(); + Mat hierarchy = new Mat(); + Imgproc.findContours(gray, contours, hierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE); + + gray.release(); + hierarchy.release(); + return contours; + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/common/SeedUtils.java b/seed-counter-desktop/src/main/java/smirnov/common/SeedUtils.java new file mode 100644 index 0000000..4fc0caf --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/common/SeedUtils.java @@ -0,0 +1,230 @@ +package smirnov.common; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.math3.stat.descriptive.rank.Percentile; +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.Point; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; + +import ru.delkom07.util.Pair; + +public class SeedUtils { + public SeedUtils() { + this(0.5, 150.0, true); + } + + public SeedUtils(double threshold, double whiteThreshold, boolean filterByArea) { + this.threshold = threshold; + this.whiteThreshold = whiteThreshold; + this.filterByArea = filterByArea; + } + + private final double MIN_AREA = 5.0; + private final double MAX_AREA = 30.0; + private final double BRIGHTNESS_PERCENTILE = 10.0; + + private final double threshold; + private final double whiteThreshold; + private final boolean filterByArea; + + private int seedNumber = 0; + private int xOffset = 0; + private int yOffset = 0; + + public double getThreshold() { + return threshold; + } + + public double getWhiteThreshold() { + return whiteThreshold; + } + + public boolean getFileterByArea() { + return filterByArea; + } + + public void setSeedNumber(int seedNumber) { + this.seedNumber = seedNumber; + } + + public int getSeedNumber() { + return seedNumber; + } + + public void setXOffset(int xOffset) { + this.xOffset = xOffset; + } + + public int getXOffset() { + return xOffset; + } + + public void setYOffset(int yOffset) { + this.yOffset = yOffset; + } + + public int getYOffset() { + return yOffset; + } + + // targets and ranges + public static final List> SEED_TYPES = Arrays.asList( + new Pair<>(new Scalar(4, 97, 108), new Scalar(50, 100, 80)), + new Pair<>(new Scalar(17, 67, 232), new Scalar(50, 50, 50)) + ); + + public static Mat getMask(Mat image, Double scale) { + Mat mask = Helper.binarizeSeed(image, SEED_TYPES); + Mat whiteMask = Helper.whiteThreshold(image); + Core.bitwise_and(mask, whiteMask, mask); + whiteMask.release(); + + int kernelSize = scale == null ? 10 : (int)(1.5 / Math.sqrt(scale)); + if (kernelSize < 10) { + kernelSize = 10; + } + Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, new Size(kernelSize, kernelSize)); + + Imgproc.morphologyEx(mask, mask, Imgproc.MORPH_CLOSE, kernel); + Imgproc.morphologyEx(mask, mask, Imgproc.MORPH_OPEN, kernel); + kernel.release(); + + return mask; + } + + public static Mat filterByMask(Mat image, Mat mask) { + return Helper.filterByMask(image, mask); + } + + public int printSeeds(Mat image, Mat imageForFilter, PrintWriter writer, + Map data, Double scale) { + List contours = Helper.getContours(imageForFilter); + Mat seedBuffer = Mat.zeros(image.rows(), image.cols(), CvType.CV_8UC1); + + for (MatOfPoint contour : contours) { + Double area = scale * Imgproc.contourArea(contour); + if (!filterByArea || (area < MAX_AREA && area > MIN_AREA)) { + List> seedData = getSeedData(contour, image, imageForFilter, seedBuffer); + if (!seedData.isEmpty()) { + data.put("seed_number", String.valueOf(seedNumber++)); + data.put("area", area.toString()); + printSeedData(data, seedData, writer); + } + } + contour.release(); + } + + seedBuffer.release(); + + return seedNumber; + } + + private List> getSeedData(MatOfPoint contour, Mat image, Mat imageForFilter, Mat seedBuffer) { + Imgproc.drawContours(seedBuffer, Collections.singletonList(contour), 0, + new Scalar(255.0), Core.FILLED); + List> result = new ArrayList<>(); + + int minX = image.cols() - 1; + int maxX = 0; + int minY = image.rows() - 1; + int maxY = 0; + + for (Point point : contour.toList()) { + if (point.x < minX) { + minX = (int) point.x; + } + if (point.x > maxX) { + maxX = (int) point.x; + } + if (point.y < minY) { + minY = (int) point.y; + } + if (point.y > maxY) { + maxY = (int) point.y; + } + } + + List minChannelValues = new ArrayList<>(); + + for (int y = minY; y <= maxY; ++y) { + for (int x = minX; x <= maxX; ++x) { + if (seedBuffer.get(y, x)[0] > 0.0) { + double[] color = image.get(y, x); + double[] colorForFilter = imageForFilter.get(y, x); + if (color[0] + color[1] + color[2] > 0.0) { + Map map = new HashMap<>(); + map.put("x", String.valueOf(x + xOffset)); + map.put("y", String.valueOf(y + yOffset)); + map.put("blue", String.valueOf(color[0])); + map.put("green", String.valueOf(color[1])); + map.put("red", String.valueOf(color[2])); + minChannelValues.add(Math.min(colorForFilter[0], Math.min(colorForFilter[1], colorForFilter[2]))); + result.add(map); + } + } + } + } + + Imgproc.drawContours(seedBuffer, Collections.singletonList(contour), 0, + new Scalar(0.0), Core.FILLED); + + if (result.size() / (maxX - minX + 1.0) / (maxY - minY + 1.0) < threshold) { + result.clear(); + } else { + Percentile percentile = new Percentile(); + percentile.setData(minChannelValues.stream().mapToDouble(x -> x).toArray()); + + if (percentile.evaluate(BRIGHTNESS_PERCENTILE) > whiteThreshold) { + result.clear(); + } + } + + return result; + } + + private void printSeedData(Map data, List> seedData, PrintWriter writer) { + for (Map map : seedData) { + for (String key : map.keySet()) { + data.put(key, map.get(key)); + } + printMap(writer, data); + } + } + + private void printMap(PrintWriter writer, Map map) { + boolean header = map.containsKey("header"); + map.remove("header"); + StringBuilder builder = new StringBuilder(); + if (header) { + for (String key : sortedKeySet(map)) { + builder.append(key); + builder.append("\t"); + } + builder.deleteCharAt(builder.length() - 1); + builder.append("\n"); + } + for (String key : sortedKeySet(map)) { + builder.append(map.get(key)); + builder.append("\t"); + } + builder.deleteCharAt(builder.length() - 1); + writer.println(builder.toString()); + } + + private List sortedKeySet(Map map) { + return map.keySet().stream().sorted().collect(Collectors.toList()); + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/regression/AbstractOLS.java b/seed-counter-desktop/src/main/java/smirnov/regression/AbstractOLS.java new file mode 100644 index 0000000..6d90cec --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/regression/AbstractOLS.java @@ -0,0 +1,117 @@ +package smirnov.regression; + +import java.nio.DoubleBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.math3.linear.Array2DRowRealMatrix; +import org.apache.commons.math3.linear.LUDecomposition; +import org.apache.commons.math3.linear.RealMatrix; +import org.apache.commons.math3.stat.regression.OLSMultipleLinearRegression; + +import smirnov.colormetric.Color; + +public abstract class AbstractOLS implements RegressionModel { + private final boolean intercept; + private double[] beta1; + private double[] beta2; + private double[] beta3; + + AbstractOLS(boolean intercept) { + this.intercept = intercept; + } + + @Override + public void train(List train, List answers) { + List answers1 = new ArrayList<>(); + List answers2 = new ArrayList<>(); + List answers3 = new ArrayList<>(); + + for (DoubleBuffer c : answers) { + answers1.add(Color.channel(c, 0)); + answers2.add(Color.channel(c, 1)); + answers3.add(Color.channel(c, 2)); + } + + beta1 = trainChannel(train, answers1); + beta2 = trainChannel(train, answers2); + beta3 = trainChannel(train, answers3); + } + + @Override + public double getTransformationDeviance(List source, List target) { + List> targetFeatures = new ArrayList<>(); + + int featuresCount = getFeatures(target.get(0)).length; + + for (int i = 0; i < featuresCount; ++i) { + targetFeatures.add(new ArrayList<>()); + } + + for (DoubleBuffer c : target) { + double[] features = getFeatures(c); + + for (int i = 0; i < features.length; ++i) { + targetFeatures.get(i).add(features[i]); + } + } + + int dim = featuresCount + (intercept ? 1 : 0); + RealMatrix matrix = new Array2DRowRealMatrix(dim, dim); + + if (intercept) { + matrix.setEntry(0, 0, 1.0); + for (int col = 1; col < dim; ++col) { + matrix.setEntry(0, col, 0.0); + } + } + + for (int index = 0; index < targetFeatures.size(); ++index) { + List answers = targetFeatures.get(index); + int row = index + (intercept ? 1 : 0); + + matrix.setRow(row, trainChannel(source, answers)); + } + + return 1.0 - new LUDecomposition(matrix).getDeterminant(); + } + + private double[] trainChannel(List trainSet, List answers) { + double[][] trainArray = new double[answers.size()][3]; + double[] answersArray = new double[answers.size()]; + + for (int i = 0; i < answers.size(); ++i) { + answersArray[i] = answers.get(i); + trainArray[i] = getFeatures(trainSet.get(i)); + } + + OLSMultipleLinearRegression regressor = new OLSMultipleLinearRegression(); + regressor.setNoIntercept(!intercept); + regressor.newSampleData(answersArray, trainArray); + + return regressor.estimateRegressionParameters(); + } + + private double getEstimate(double[] features, double[] beta) { + double answer = (intercept ? 1 : 0) * beta[0]; + for (int i = 0; i < features.length; ++i) { + answer += features[i] * beta[i + (intercept ? 1 : 0)]; + } + + return answer; + } + + @Override + public void calibrate(DoubleBuffer color) { + double[] features = getFeatures(color); + color.put(new double[] {getEstimate(features, beta1), + getEstimate(features, beta2), getEstimate(features, beta3)}); + } + + @Override + public String getName() { + return this.getClass().getSimpleName() + (intercept ? "Intercept" : ""); + } + + abstract protected double[] getFeatures(DoubleBuffer color); +} diff --git a/seed-counter-desktop/src/main/java/smirnov/regression/ColorSpace.java b/seed-counter-desktop/src/main/java/smirnov/regression/ColorSpace.java new file mode 100644 index 0000000..194e563 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/regression/ColorSpace.java @@ -0,0 +1,87 @@ +package smirnov.regression; + +import java.nio.DoubleBuffer; + +import smirnov.colormetric.Color; + +public enum ColorSpace { + RGB(false, false), + RGB_LINEAR(false, true), + XYZ(true, false), + XYZ_LINEAR(true, true); + + private final boolean isXYZ; + private final boolean isLinear; + + ColorSpace(boolean isXYZ, boolean isLinear) { + this.isXYZ = isXYZ; + this.isLinear = isLinear; + } + + private static double linearizeRGB(double channelColor) { + channelColor /= 255.0; + if (channelColor > 0.04045) { + channelColor = Math.pow((channelColor + 0.055) / 1.055, 2.4); + } else { + channelColor /= 12.92; + } + + return channelColor * 100.0; + } + + private static double inverseLinearizeRGB(double channelColor) { + channelColor /= 100.0; + if (channelColor > 0.0031308) { + channelColor = 1.055 * Math.pow(channelColor, 1.0 / 2.4) - 0.055; + } else { + channelColor *= 12.92; + } + + return channelColor * 255.0; + } + + public DoubleBuffer convertFromBGR(DoubleBuffer color, boolean inplace) { + if (!inplace) { + color = DoubleBuffer.wrap(new double[] {Color.channel(color, 0), + Color.channel(color, 1), Color.channel(color, 2)}); + } + + double b = Color.channel(color, 0); + double g = Color.channel(color, 1); + double r = Color.channel(color, 2); + + if (isLinear) { + b = linearizeRGB(b); + g = linearizeRGB(g); + r = linearizeRGB(r); + } + + if (isXYZ) { + color.put(color.position(), r * 0.4124 + g * 0.3576 + b * 0.1805); + color.put(color.position() + 1, r * 0.2126 + g * 0.7152 + b * 0.0722); + color.put(color.position() + 2, r * 0.0193 + g * 0.1192 + b * 0.9505); + } else { + color.put(color.position(), b); + color.put(color.position() + 1, g); + color.put(color.position() + 2, r); + } + + return color; + } + + public void convertToBGR(DoubleBuffer color) { + if (isXYZ) { + double x = Color.channel(color, 0); + double y = Color.channel(color, 1); + double z = Color.channel(color, 2); + color.put(color.position(), 0.0556434 * x - 0.2040259 * y + 1.0572252 * z); + color.put(color.position() + 1, -0.9692660 * x + 1.8760108 * y + 0.0415560 * z); + color.put(color.position() + 2, 3.2404542 * x - 1.5371385 * y - 0.4985314 * z); + } + if (isLinear) { + color.put(color.position(), inverseLinearizeRGB(Color.channel(color, 0))); + color.put(color.position() + 1, inverseLinearizeRGB(Color.channel(color, 1))); + color.put(color.position() + 2, inverseLinearizeRGB(Color.channel(color, 2))); + } + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/regression/IdentityModel.java b/seed-counter-desktop/src/main/java/smirnov/regression/IdentityModel.java new file mode 100644 index 0000000..87fe3c6 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/regression/IdentityModel.java @@ -0,0 +1,22 @@ +package smirnov.regression; + +import java.nio.DoubleBuffer; +import java.util.List; + +public class IdentityModel implements RegressionModel { + @Override + public void train(List train, List answers) {} + + @Override + public double getTransformationDeviance(List source, List target) { + return 0.0; + } + + @Override + public void calibrate(DoubleBuffer c) {} + + @Override + public String getName() { + return "Identity"; + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/regression/RegressionFactory.java b/seed-counter-desktop/src/main/java/smirnov/regression/RegressionFactory.java new file mode 100644 index 0000000..a55e4d5 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/regression/RegressionFactory.java @@ -0,0 +1,32 @@ +package smirnov.regression; + +public class RegressionFactory { + public static RegressionModel createModel(Order order) { + if (order == Order.IDENTITY) { + return new IdentityModel(); + } + switch(order.order) { + case 1: return new SimpleOLS(order.intercept); + case 2: return new SecondOrderOLS(order.intercept); + default: return new ThirdOrderOLS(order.intercept); + } + } + + public enum Order { + IDENTITY(0, false), + FIRST(1, false), + FIRST_INTERCEPT(1, true), + SECOND(2, false), + SECOND_INTERCEPT(2, true), + THIRD(3, false), + THIRD_INTERCEPT(3, true); + + final int order; + final boolean intercept; + + Order(int order, boolean intercept) { + this.order = order; + this.intercept = intercept; + } + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/regression/RegressionModel.java b/seed-counter-desktop/src/main/java/smirnov/regression/RegressionModel.java new file mode 100644 index 0000000..036b91c --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/regression/RegressionModel.java @@ -0,0 +1,12 @@ +package smirnov.regression; + +import java.nio.DoubleBuffer; +import java.util.List; + +public interface RegressionModel { + void train(List train, List answers); + // calculates (1 - det[H]) metric, where H is the transformation matrix from source to target features + double getTransformationDeviance(List source, List target); + void calibrate(DoubleBuffer c); + String getName(); +} diff --git a/seed-counter-desktop/src/main/java/smirnov/regression/SecondOrderOLS.java b/seed-counter-desktop/src/main/java/smirnov/regression/SecondOrderOLS.java new file mode 100644 index 0000000..5c9fb04 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/regression/SecondOrderOLS.java @@ -0,0 +1,24 @@ +package smirnov.regression; + +import java.nio.DoubleBuffer; + +import smirnov.colormetric.Color; + +public class SecondOrderOLS extends AbstractOLS implements RegressionModel { + public SecondOrderOLS(boolean intercept) { + super(intercept); + } + + @Override + protected double[] getFeatures(DoubleBuffer color) { + double channel0 = Color.channel(color, 0); + double channel1 = Color.channel(color, 1); + double channel2 = Color.channel(color, 2); + + return new double[] { + channel0, channel1, channel2, + channel0 * channel0, channel0 * channel1, channel0 * channel2, + channel1 * channel1, channel1 * channel2, channel2 * channel2 + }; + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/regression/SimpleOLS.java b/seed-counter-desktop/src/main/java/smirnov/regression/SimpleOLS.java new file mode 100644 index 0000000..76a3ce6 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/regression/SimpleOLS.java @@ -0,0 +1,22 @@ +package smirnov.regression; + +import java.nio.DoubleBuffer; + +import smirnov.colormetric.Color; + +public class SimpleOLS extends AbstractOLS implements RegressionModel { + public SimpleOLS(boolean intercept) { + super(intercept); + } + + @Override + protected double[] getFeatures(DoubleBuffer color) { + double channel0 = Color.channel(color, 0); + double channel1 = Color.channel(color, 1); + double channel2 = Color.channel(color, 2); + + return new double[] { + channel0, channel1, channel2 + }; + } +} diff --git a/seed-counter-desktop/src/main/java/smirnov/regression/ThirdOrderOLS.java b/seed-counter-desktop/src/main/java/smirnov/regression/ThirdOrderOLS.java new file mode 100644 index 0000000..cb62895 --- /dev/null +++ b/seed-counter-desktop/src/main/java/smirnov/regression/ThirdOrderOLS.java @@ -0,0 +1,29 @@ +package smirnov.regression; + +import java.nio.DoubleBuffer; + +import smirnov.colormetric.Color; + +public class ThirdOrderOLS extends AbstractOLS implements RegressionModel { + public ThirdOrderOLS(boolean intercept) { + super(intercept); + } + + @Override + protected double[] getFeatures(DoubleBuffer color) { + double channel0 = Color.channel(color, 0); + double channel1 = Color.channel(color, 1); + double channel2 = Color.channel(color, 2); + + return new double[] { + channel0, channel1, channel2, + channel0 * channel0, channel0 * channel1, channel0 * channel2, + channel1 * channel1, channel1 * channel2, channel2 * channel2, + channel0 * channel0 * channel0, channel0 * channel0 * channel1, + channel0 * channel0 * channel2, channel0 * channel1 * channel1, + channel0 * channel1 * channel2, channel0 * channel2 * channel2, + channel1 * channel1 * channel1, channel1 * channel1 * channel2, + channel1 * channel2 * channel2, channel2 * channel2 * channel2, + }; + } +} diff --git a/seed-counter-desktop/src/main/resources/context.xml b/seed-counter-desktop/src/main/resources/context.xml new file mode 100644 index 0000000..16cc3e2 --- /dev/null +++ b/seed-counter-desktop/src/main/resources/context.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/seed-counter-desktop/src/main/resources/templates/color_checker/reference.png b/seed-counter-desktop/src/main/resources/templates/color_checker/reference.png new file mode 100644 index 0000000..8c5a862 Binary files /dev/null and b/seed-counter-desktop/src/main/resources/templates/color_checker/reference.png differ diff --git a/seed-counter-desktop/src/test/java/ru/delkom07/AppTest.java b/seed-counter-desktop/src/test/java/ru/delkom07/AppTest.java new file mode 100644 index 0000000..1a9313b --- /dev/null +++ b/seed-counter-desktop/src/test/java/ru/delkom07/AppTest.java @@ -0,0 +1,188 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package ru.delkom07; + +import org.junit.jupiter.api.Test; + +class AppTest { + @Test void appHasAGreeting() { + //App classUnderTest = new App(); + //assertNotNull(classUnderTest.getGreeting(), "app should have a greeting"); + } + + + @Test void generalTest() { + String[] args = new String[]{"../../../workspace/data/itmi_2023/input", "--out", "../../../workspace/data/itmi_2023/output", "-cc", "-ws", "-dr", "-cd"}; + +// SpringApplication.run(DesktopMainWithColorDescriptorsOneThread.class, args); +// ApplicationContext context = new ClassPathXmlApplicationContext("context.xml"); +// Bean bean = (Bean) context.getBean("bean"); +// System.out.println(bean.getName()); +// +// +// +// System.out.println("Start"); +// System.out.println("Length: " + args.length); +// System.out.println(Arrays.toString(args)); +// +// OpenCV.loadLocally(); +// +// long time = System.currentTimeMillis(); +// +// try { +// // processing of command line +// List optMap = new ArrayList(2); +// optMap.add(new ArgOption("--help", "-h", true)); // print help +// optMap.add(new ArgOption("--out", "-o", false)); // set output directory +// +// optMap.add(new ArgOption("--colorchecker", "-cc", true)); // image contains ColorChecker +// optMap.add(new ArgOption("--calibrate", "-cl", true)); // do color calibration with ColorChecker +// optMap.add(new ArgOption("--withoutsheet", "-ws", true)); // background is solid. standard sheet of paper is not used for scale definition. +// +// optMap.add(new ArgOption("--colordescriptors", "-cd", true)); // calculate color descriptors +// optMap.add(new ArgOption("--draw", "-dr", true)); // draw additional output images +// optMap.add(new ArgOption("--markdescriptor", "-md", false)); // mark HSV color descriptor on output image +// +// optMap.add(new ArgOption("--potatoes", "-pt", true)); // +// +// CommandRepresentation cmd = new CommandRepresentation(args, optMap, optMap.size(), 0, 2000, 0); +// +// File outputDir = handleOptions(cmd); +// +// // preparing input files +// File inputDir = new File(cmd.getAcceptedArgs()[0]); +// +// String base = "output"; +// String extention = "tsv"; +// File outputDataFile = new File(outputDir, base + "." + extention); +// +// +// PrintWriter writer = new PrintWriter(outputDataFile, "UTF-8"); +// writeFileHead(writer); +// +// ImageWorkspace workspace = new ImageWorkspace(inputDir, outputDir, true); +// List opUnits = workspace.getRawOpUnits(); +// +// for(OpUnit pair : opUnits) { +// File inFile = pair.getIn(); +// File outFile = pair.getOut(); +// +// AddInfo info = AddInfo.fromFilesStructure(inFile); +// +// System.out.println(inFile.getCanonicalPath()); +// +// try { +// +// Mat srcImg = Imgcodecs.imread(inFile.getCanonicalPath(), Imgcodecs.CV_LOAD_IMAGE_UNCHANGED); +// +// if(srcImg.empty()) +// throw new IncorrectCmdException("Input file is not image."); +// +// Mat region = srcImg; +// +// if(!withoutsheet) { +// Quad quad = detectProcessor.detectQuad(srcImg); +// +// if(null!=quad) { +// region = detectProcessor.getTransformedField(srcImg, quad); +// } else { +// System.out.print("Input file:" + inFile.getName()+". "); +// System.out.println("Sheet isn't found. Skipping image."); +// +// continue; +// } +// } +// +// Imgcodecs.imwrite(outFile.getCanonicalPath().replace(".png", "_transformed.png"), region); +// +// List containers; +// +// // if is colorchecker +// Double transformationDeviance = 0.0; +// if(colorchecker) { +// MyColorChecker myColorChecker = new MyColorChecker(region); +// +// // fill ColorChecker +// myColorChecker.fillColorChecker(); +// +// +// containers = detectProcessor.detectSeedContours(region, myColorChecker.getPixPerMM()); +// +// if(calibrate) { +// Pair calibratedPair = myColorChecker.calibrateImg(); +// +// //if(!region.empty()) { // STUB: need to release not calibrated region +// // region.release(); +// //} +// +// region = calibratedPair.getLeft(); +// transformationDeviance = calibratedPair.getRight(); +// +// //System.out.println("# Transformation deviance: " + transformationDeviance.toString()); +// } +// } else { +// containers = detectProcessor.detectSeedContours(region.clone()); +// } +// +// //System.out.println("Seed count: " + containers.size()); +// +// saveResults(writer, region, containers, inFile.getName(), info, transformationDeviance); +// JSONSaver.saveInJSON(region, containers, inFile, outFile); +// +// if(draw) { +// if(markdescriptor) { +// drawContoursWithRectAndNumAndMarkGCH(region, containers, colorSpace, markGCHDimension, markGCHBinNumber); +// } else { +// drawContoursWithRectAndNum(region, containers); +// } +// +// Imgcodecs.imwrite(outFile.getCanonicalPath(), region); +// } +// +// +// // Releases +// for(PlanarObject container : containers) { +// container.getContour().release(); +// } +// containers.clear(); +// +// if(!srcImg.empty()) { +// srcImg.release(); +// } +// +// if(!region.empty()) { +// region.release(); +// } +// +// pair.setProcessed(); +// System.out.println(inFile.getParent() + "/" + inFile.getName() + " done."); +// +// } catch(IncorrectCmdException ex) { +// System.exit(-1); +// } catch (SeedCounterProcessorException e) { +// e.printStackTrace(); +// continue; +// } catch (IOException e) { +// e.printStackTrace(); +// continue; +// } +// } +// +// +// writer.close(); +// +// System.out.println("Computation time: " + Integer.toString((int)(System.currentTimeMillis() - time)/1000)); +// +// System.exit(0); // STATUS - NORM +// +// } catch (IncorrectCmdException e) { +// System.err.println("Error: "+e.getMessage()); +// usage(e.getMessage()); +// } catch (IOException e) { +// System.err.println("Error: "+e.getMessage()); +// System.out.println("IO error. The program will be terminated"); +// System.exit(1); +// } + } +} diff --git a/seed-counter-desktop/src/test/java/ru/delkom07/WorkspaceTest.java b/seed-counter-desktop/src/test/java/ru/delkom07/WorkspaceTest.java new file mode 100644 index 0000000..5708985 --- /dev/null +++ b/seed-counter-desktop/src/test/java/ru/delkom07/WorkspaceTest.java @@ -0,0 +1,42 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package ru.delkom07; + +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import ru.delkom07.fileutils.FileUtilities; +import ru.delkom07.workspaces.SimpleWorkspace; + +class WorkspaceTest { + @Test void simpleWorkspace() throws IOException { + File inputDir = new File("src/test/resources/workspace/input"); + File outputDir = new File("src/test/resources/workspace/output"); + + assertTrue(inputDir.exists()); + assertTrue(outputDir.exists()); + + SimpleWorkspace simpleWorkspace = new SimpleWorkspace(inputDir, outputDir, true, FileUtilities.imageFilter, null); + List units = simpleWorkspace.getOpUnits(); + + assertEquals(units.size(), 2, "There should be 2 file in resource test input dir (all recurcively searched)."); + assertEquals(units.get(0).getIn().getName(), "input.jpg"); + assertEquals(units.get(1).getIn().getName(), "subInput.jpg"); + + + System.out.println(units.get(0).getOut().getCanonicalPath()); + + } + + + @Test void generalTest() { + + } +} diff --git a/seed-counter-desktop/src/test/resources/workspace/input/input.jpg b/seed-counter-desktop/src/test/resources/workspace/input/input.jpg new file mode 100644 index 0000000..e69de29 diff --git a/seed-counter-desktop/src/test/resources/workspace/input/subdir/subInput.jpg b/seed-counter-desktop/src/test/resources/workspace/input/subdir/subInput.jpg new file mode 100644 index 0000000..e69de29 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e3d3fd4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,14 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/7.5/userguide/multi_project_builds.html + */ + +rootProject.name = 'wheat-projects' + +include('seed-counter-desktop') +include('mask-color-descriptors') +include('utils') diff --git a/utils/build.gradle b/utils/build.gradle new file mode 100644 index 0000000..9ec0146 --- /dev/null +++ b/utils/build.gradle @@ -0,0 +1,39 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java library project to get you started. + * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle + * User Manual available at https://docs.gradle.org/7.5/userguide/building_java_projects.html + */ + +plugins { + // Apply the java-library plugin for API and implementation separation. + id 'java-library' +} + +compileJava.options.encoding = 'UTF-8' +compileTestJava.options.encoding = 'UTF-8' + + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + // Use JUnit Jupiter for testing. + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' + + implementation("org.openpnp:opencv:3.4.2-0") + + // This dependency is exported to consumers, that is to say found on their compile classpath. + //api 'org.apache.commons:commons-math3:3.6.1' + + // This dependency is used internally, and not exposed to consumers on their own compile classpath. + //implementation 'com.google.guava:guava:31.0.1-jre' +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} diff --git a/utils/src/main/java/ru/delkom07/Library.java b/utils/src/main/java/ru/delkom07/Library.java new file mode 100644 index 0000000..d7323b9 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/Library.java @@ -0,0 +1,10 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package ru.delkom07; + +public class Library { + public boolean someLibraryMethod() { + return true; + } +} diff --git a/utils/src/main/java/ru/delkom07/fileutils/FileUtilities.java b/utils/src/main/java/ru/delkom07/fileutils/FileUtilities.java new file mode 100644 index 0000000..85acb47 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/fileutils/FileUtilities.java @@ -0,0 +1,104 @@ +package ru.delkom07.fileutils; + +import java.io.File; +import java.io.FileFilter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class FileUtilities { + public static FileFilter imageFilter = new FileFilter() { + @Override + public boolean accept(File pathname) { + if(pathname.getName().matches(".+\\.(png|PNG|jpg|JPG|bmp|BMP)")) { + return true; + } + return false; + } + }; + + + public static FileFilter jpgImageFilter = new FileFilter() { + @Override + public boolean accept(File pathname) { + if(pathname.getName().matches(".+\\.(jpg|JPG)")) { + return true; + } + return false; + } + }; + + + public static FileFilter pngImageFilter = new FileFilter() { + @Override + public boolean accept(File pathname) { + if(pathname.getName().matches(".+\\.(png|PNG)")) { + return true; + } + return false; + } + }; + + + public static FileFilter bmpImageFilter = new FileFilter() { + @Override + public boolean accept(File pathname) { + if(pathname.getName().matches(".+\\.(bmp|BMP)")) { + return true; + } + return false; + } + }; + + + public static FileFilter fileFilter = new FileFilter() { + @Override + public boolean accept(File pathname) { + if(pathname.isFile()) { + return true; + } + return false; + } + }; + + public static FileFilter dirFilter = new FileFilter() { + @Override + public boolean accept(File pathname) { + if(pathname.isDirectory()) { + return true; + } + return false; + } + }; + + + /** + * Получить все файлы, соответствующие fileFilter, из директории и поддиректорий рекурсивно. + * @param folder - корневая директория. + * @param fileFilter (can be null) - фильтр файлов. + * @return + */ + public static List getFilesRecursively(File folder, FileFilter fileFilter) { + if(null == fileFilter) { + fileFilter = FileUtilities.fileFilter; + } + + List imagesList = new ArrayList(Arrays.asList(folder.listFiles(fileFilter))); + + File[] subFolders = folder.listFiles(new FileFilter() { + + @Override + public boolean accept(File pathname) { + if(pathname.isDirectory()) + return true; + return false; + } + }); + + for(File subFolder : subFolders) { + imagesList.addAll(getFilesRecursively(subFolder, fileFilter)); + } + + return imagesList; + } +} diff --git a/utils/src/main/java/ru/delkom07/fileutils/Main.java b/utils/src/main/java/ru/delkom07/fileutils/Main.java new file mode 100644 index 0000000..09b5155 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/fileutils/Main.java @@ -0,0 +1,25 @@ +package ru.delkom07.fileutils; + +import java.io.File; +import java.io.IOException; + +public class Main { + + public static void main(String[] args) throws IOException { + //FileUtils.compareTwoFouldersIgnoreFileNames(new File("../../wwork/2018_09_10_"), new File("../../wwork/2018_09_10_renamed_")); + + //FileUtils.searchRepeatsInFolderIgnoreFileNames(new File("../data/img_renaming/renamed")); + + //FileUtils.checkFilesForAnnotations(new File("../data/img_renaming/annotations.txt"), new File("../data/img_renaming/renamed")); + + //FileUtils.checkAnnotationsForFiles(new File("../data/img_renaming/renamed"), new File[] {new File("../data/img_renaming/annotations.txt")}); + + //FileUtils.removeFileWithDoublecatesNames(); + + //FileUtils.renameFilesForAnnotations(new File("../data/img_renaming/annotations.csv"), new File("../data/img_renaming/renamed")); + + + //FileUtils.findAndCopyImgsFromList(new File("c:\\Users\\KomyshevEG\\Downloads\\renamed_19.12.18"), new File("d:\\workspace\\data\\wheat_ears\\input\\calibrated_problem"), new File("d:\\workspace\\data\\wheat_ears\\input\\calibrated_problem\\list.txt")); + } + +} diff --git a/utils/src/main/java/ru/delkom07/geometry/AdditionalIndexies.java b/utils/src/main/java/ru/delkom07/geometry/AdditionalIndexies.java new file mode 100644 index 0000000..0f630da --- /dev/null +++ b/utils/src/main/java/ru/delkom07/geometry/AdditionalIndexies.java @@ -0,0 +1,185 @@ +package ru.delkom07.geometry; + +import java.util.Arrays; + +import org.opencv.core.Core; +import org.opencv.core.Core.MinMaxLocResult; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfInt; +import org.opencv.core.MatOfPoint; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.RotatedRect; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; + +import ru.delkom07.geometry.obj.Circle; + + +public class AdditionalIndexies { + + public static double circularityIndex(MatOfPoint contour) { + double area = Imgproc.contourArea(contour); + + MatOfPoint2f contour2f = new MatOfPoint2f( contour.toArray() ); + double perimeter = Imgproc.arcLength(contour2f, true); + + return ( 4*Math.PI * area ) / (perimeter*perimeter); + } + + + public static double roundness(MatOfPoint contour) { + MatOfPoint2f forFillEllipse = new MatOfPoint2f(); + contour.convertTo(forFillEllipse, CvType.CV_32FC2); + RotatedRect rotatedRect = Imgproc.fitEllipse(forFillEllipse); + double majorAxis = Math.max(rotatedRect.size.height, rotatedRect.size.width); + + double area = Imgproc.contourArea(contour); + + return (4*area) / (Math.PI * majorAxis*majorAxis); // (4*area) / (pi*[Major axis]^2) + } + + + public static double solidity(MatOfPoint contour) { + double area = Imgproc.contourArea(contour); + + Point[] contourArr = contour.toArray(); + MatOfInt hull = new MatOfInt(); + Imgproc.convexHull(contour, hull); + int[] hullIntArr = hull.toArray(); + + Point[] hullArr = new Point[hullIntArr.length]; + for(int i=0; i contours) { + if(null != contours && 0 != contours.size()) { + double maxArea = Imgproc.contourArea(contours.get(0)); + MatOfPoint bestContour = contours.get(0); + + for(MatOfPoint contour : contours) { + double area = Imgproc.contourArea(contour); + + if(area > maxArea) { + maxArea = area; + bestContour = contour; + } + } + + return bestContour; + } else { + return null; + } + } + + + /** + * Get linear sizes of object with elliptic contour. + * @param contour - object contour. + * @return linear sizes. + */ + private static double[] getLengthAndWidth(MatOfPoint contour) { + // length and width + MatOfPoint2f forFillEllipse = new MatOfPoint2f(); + contour.convertTo(forFillEllipse, CvType.CV_32FC2); + RotatedRect rotatedRect = Imgproc.fitEllipse(forFillEllipse); + Point[] points = new Point[4]; + rotatedRect.points(points); + double length = 0; + double width = 0; + length = SimpleGeometry.distance(points[0], points[1]); + width = SimpleGeometry.distance(points[1], points[2]); + + if(length points = contour.toList(); + for(Point p : points) { + acc[0] += p.x; + acc[1] += p.y; + } + + return new double[]{acc[0]/points.size(), acc[1]/points.size()}; + } + + +// **Obsolete version +// public static double[] massCenter(MatOfPoint contour) { +// double[] massCenter = new double[2]; +// massCenter[0] = 0.0; +// massCenter[1] = 0.0; +// int count = 0; +// for(int i=0; i getVectors(MatOfPoint contour, int vectSize) { + List vectList = new LinkedList(); // список векторов (только направление, без привязки к координатам) + //List vectEndList = new LinkedList(); // список концов векторов (для привязки к координатам) + double[] prevPoint = contour.get(0, 0); + int step = 0; + for(int i=0; i=vectSize) { // шаг увеличивается пока не достигнет необходимой длины вектора + step=0; + + double[] curPoint = contour.get(j, i); + + vectList.add(new Vector(prevPoint[0], prevPoint[1], curPoint[0], curPoint[1])); + + prevPoint = curPoint; + } + } + } + + // Последнй вектор замыкает контур: + double[] curPoint = contour.get(0, 0); + vectList.add(new Vector(prevPoint[0], prevPoint[1], curPoint[0], curPoint[1])); + + return vectList; + } + + + /** + * Вычисление перегибов + * @param vectors - список векторов, идущих друг за другим (каждый последующий выходит из конца предидущего), + * и образующих замкнутый контур. + * @return - списк векторов, на конце которых обнаружился перегиб. + * @throws Exception + */ + public static List> getBendsIndxs(List vectors) { + // общая сумма направлений необходима, чтобы определить в какую сторону производится обход контура + double courseSum = 0.0; + + // список направлений векторов относительно предыдущего вектора: правее или левее + List> courseList = new LinkedList>(); + Vector prevVector = vectors.get(vectors.size()-1); + for(int i=0; i 1; + Vector curVector = vectors.get(i); + + // направление текущего вектора относительно предидущего: правее (course<0) или левее (course>0) + Double relativeCourse = prevVector.x()*curVector.y()-prevVector.y()*curVector.x(); + + courseList.add(new Pair(relativeCourse, SimpleGeometry.angle(prevVector, curVector))); + + courseSum += relativeCourse; + + prevVector = curVector; + } + + // выбор индексов векторов, где обнаружен перегиб в обратную сторону относительно направления обхода контура + + + List> bendListIndxs = new LinkedList>(); + for(int i=0; i coursePair = courseList.get(i); + double course = coursePair.getLeft(); + double angle = coursePair.getRight(); + + double thresAngle = 0.7; + if(courseSum > 0) { + if(course < 0 && angle > thresAngle) { // Math.abs(course)>thres + Vector v1 = i>0 ? vectors.get(i-1) : vectors.get(vectors.size()-1); + Vector v2 = i(new Point(vectors.get(i).x1(), vectors.get(i).y1()), v3 )); + } + } else { + if(course > 0 && angle > thresAngle) { // Math.abs(course)>thres + Vector v1 = i>0 ? vectors.get(i-1) : vectors.get(vectors.size()-1); + Vector v2 = i(new Point(vectors.get(i).x1(), vectors.get(i).y1()), v3 )); + } + } + } + + return bendListIndxs; + } + + + +} + + + + + +// TO CHECK +//private static double[] getMassCenter(MatOfPoint contour) { +//// mass center +//double[] massCenter = new double[2]; +//massCenter[0] = 0.0; +//massCenter[1] = 0.0; +//int count = 0; +//for(int i=0; i points) { + double minX = Integer.MAX_VALUE; + double minY = Integer.MAX_VALUE; + + double maxX = -1; + double maxY = -1; + for(Point point : points) { + if(point.x < minX) { + minX = point.x; + } + + if(point.y < minY) { + minY = point.y; + } + + if(point.x > maxX) { + maxX = point.x; + } + + if(point.y > maxY) { + maxY = point.y; + } + } + + return new Point[] {new Point(minX, minY), new Point(maxX, maxY)}; + } + + + /** + * + * @param pointArr + * @return + */ + private static Point[] findMaxDistantPoints(Point[] pointArr) { + Point maxP1 = pointArr[0]; + Point maxP2 = pointArr[0]; + double maxDistance = 0; + + for(Point p1: pointArr) { + for(Point p2: pointArr) { + if(distance(p1, p2) > maxDistance) { + maxP1 = p1; + maxP2 = p2; + maxDistance = distance(p1, p2); + } + } + + } + + return new Point[]{maxP1, maxP2}; + } + + + /** + * Вернить две точки лежащие наиболее близко к данной линии и нормали справа и слева от нормали соответственно. + * Функция сначала находит список точек претендентов: наиболее близкие к линии точки по pointBufferSize справа и слева. + * После этого для каждой стороны выбирается наиболее близкая точка к нормали. + * @param listOfPoints - списко точек. + * @param line - данная линия. + * @param normal - нормаль. + * @return две точки лежащие наиболее близко к данной линии и нормали справа и слева от нормали соответственно. + */ + private static Point[] getTwoPointThatLayOnGivenLine(List listOfPoints, Vector line, Vector normal) { + short pointBufferSize = 5; + + List> leftPoints = new LinkedList>(); + List> rightPoints = new LinkedList>(); + + for(Point point : listOfPoints) { + double xDiff = (line.x2() - line.x1()); + double yDiff = (line.y2() -line.y1()); + + xDiff = xDiff == 0 ? 0.0001 : xDiff; + yDiff = yDiff == 0 ? 0.0001 : yDiff; + + double closenessToLine = Math.abs((point.x - line.x1())/xDiff - (point.y - line.y1())/yDiff); + //double closenessToLine = Math.abs((point.x - line.x1())/(line.x2() - line.x1()) - (point.y - line.y1())/(line.y2()-line.y1())); + + // position relative to the normal + // (y1-y2)*x+(x2-x1)*y+(x1*y2-x2*y1) > or < 0 + double position = (normal.y1()-normal.y2())*point.x + (normal.x2()-normal.x1()) * point.y + (normal.x1()*normal.y2()-normal.x2()*normal.y1()); + + Comparator> comparator = new Comparator>() { + + @Override + public int compare(Pair arg0, Pair arg1) { + if(arg0.getRight() > arg1.getRight()) { + return 1; + } + if(arg0.getRight() < arg1.getRight()) { + return -1; + } + return 0; + } + }; + + if(position <= 0) { // right position + if(rightPoints.size() < pointBufferSize && !Double.isNaN(closenessToLine) /*&& Double.isFinite(closenessToLine)*/) { + rightPoints.add(new Pair(point, closenessToLine)); + + if(rightPoints.size() == pointBufferSize) { + //rightPoints.sort(comparator); + Collections.sort(rightPoints, comparator); + } + + } else { + for(int j=0; j rightPoint = rightPoints.get(j); + + if(closenessToLine < rightPoint.getRight()) { + rightPoints.add(rightPoints.indexOf(rightPoint), new Pair(point, closenessToLine)); + rightPoints.remove(rightPoints.size()-1); + break; + } + } + + } + + } else { // left position + if(leftPoints.size() < pointBufferSize && !Double.isNaN(closenessToLine) /* && Double.isFinite(closenessToLine) */) { + leftPoints.add(new Pair(point, closenessToLine)); + + if(leftPoints.size() == pointBufferSize) { + //leftPoints.sort(comparator); + Collections.sort(leftPoints, comparator); + } + } else { + for(int j=0; j leftPoint = leftPoints.get(j); + + if(closenessToLine < leftPoint.getRight()) { + leftPoints.add(leftPoints.indexOf(leftPoint), new Pair(point, closenessToLine)); + leftPoints.remove(leftPoints.size()-1); + break; + } + } + + } + } + } + + + Point rightPoint = null; + double rightBestDist = Double.MAX_VALUE; + for(Pair rightPointPair : rightPoints) { + Point point = rightPointPair.getLeft(); + + double xDiff = (normal.x2() - normal.x1()); + double yDiff = (normal.y2() - normal.y1()); + + xDiff = xDiff == 0 ? 0.0001 : xDiff; + yDiff = yDiff == 0 ? 0.0001 : yDiff; + + double closenessToNormal = Math.abs((point.x - normal.x1())/xDiff - (point.y - normal.y1())/yDiff); + //double closenessToNormal = Math.abs((point.x - normal.x1())/(normal.x2() - normal.x1()) - (point.y - normal.y1())/(normal.y2()-normal.y1())); + + if(closenessToNormal < rightBestDist) { + rightPoint = point; + rightBestDist = closenessToNormal; + } + + } + + Point leftPoint = null; + double leftBestDist = Double.MAX_VALUE; + for(Pair leftPointPair : leftPoints) { + Point point = leftPointPair.getLeft(); + + double xDiff = (normal.x2() - normal.x1()); + double yDiff = (normal.y2()- normal.y1()); + + xDiff = xDiff == 0 ? 0.0001 : xDiff; + yDiff = yDiff == 0 ? 0.0001 : yDiff; + + double closenessToNormal = Math.abs((point.x - normal.x1())/xDiff - (point.y - normal.y1())/yDiff); + //double closenessToNormal = Math.abs((point.x - normal.x1())/(normal.x2() - normal.x1()) - (point.y - normal.y1())/(normal.y2()-normal.y1())); + + if(closenessToNormal < leftBestDist) { + leftPoint = point; + leftBestDist = closenessToNormal; + } + } + + return new Point[]{leftPoint, rightPoint}; + } + + + private static double distanceBetweenPointsOfElongatedVectors(Vector v1, Vector v2, double len) { + Vector v1Direction = v1.normalizedDirection(); + Vector v2Direction = v2.normalizedDirection(); + + Point p1 = new Point(v1.x1() + v1Direction.x()*len, v1.y1() + v1Direction.y()*len); + Point p2 = new Point(v2.x1() + v2Direction.x()*len, v2.y1() + v2Direction.y()*len); + + return distance(p1, p2); + } +} diff --git a/utils/src/main/java/ru/delkom07/geometry/SpecificGeometry.java b/utils/src/main/java/ru/delkom07/geometry/SpecificGeometry.java new file mode 100644 index 0000000..161bb73 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/geometry/SpecificGeometry.java @@ -0,0 +1,341 @@ +package ru.delkom07.geometry; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.opencv.core.MatOfPoint; +import org.opencv.core.Point; + +import ru.delkom07.geometry.obj.LineFunction; + +public class SpecificGeometry { + public static Point[] getTopLeftAndBottomRight(List points) { + double minX = Integer.MAX_VALUE; + double minY = Integer.MAX_VALUE; + + double maxX = -1; + double maxY = -1; + for(Point point : points) { + if(point.x < minX) { + minX = point.x; + } + + if(point.y < minY) { + minY = point.y; + } + + if(point.x > maxX) { + maxX = point.x; + } + + if(point.y > maxY) { + maxY = point.y; + } + } + + return new Point[] {new Point(minX, minY), new Point(maxX, maxY)}; + } + + + /** + * Разделение контура на два по линии представленной двумя точками данного контура. + * @param contour - исходный контур. + * @param point1 - первая точка линии деления. + * @param point2 - вторая точка линии деления. + * @return - два контура, замкнутые по линии деления. + */ + private static MatOfPoint[] devideCountour(MatOfPoint contour, Point point1, Point point2) { + MatOfPoint contour1 = new MatOfPoint(); + MatOfPoint contour2 = new MatOfPoint(); + + List pointsOrdered = contour.toList(); + + List pointsOrdered1 = new LinkedList(); + List pointsOrdered2 = new LinkedList(); + + // переключатель контура + boolean contourRelay = false; + for(Point point : pointsOrdered) { + // дошли до точки линии деления + if( point.equals(point1) || point.equals(point2)) { + contourRelay = !contourRelay; + + // добавить все точки по линии деления для замыкания контура + if(point.equals(point1)) { // from point1 to point2 + addMiddlePoints(contourRelay ? pointsOrdered1 : pointsOrdered2, point1, point2); + } else { + addMiddlePoints(contourRelay ? pointsOrdered1 : pointsOrdered2, point2, point1); + } + } + + // добавить текущую точку исходного контура в соответствующий новый контур + if(contourRelay) { + pointsOrdered1.add(point); + } else { + pointsOrdered2.add(point); + } + } + + contour1.fromList(pointsOrdered1); + contour2.fromList(pointsOrdered2); + + return new MatOfPoint[]{contour1, contour2}; + } + + + private static void addMiddlePoints(List pointsOrdered, Point point1, Point point2) { + double alfa; + + int stepCount = (int) SimpleGeometry.distance(point1, point2) * 2; + double step = 1.0 / stepCount; + + for(int i=0; i distancesWithPoints = new HashMap (); + + // Find all distance for all point to middle normal + for(Point p: contour.toList()) { + double distance = distanceFromPointToMiddleNormal(new double[] {p.x, p.y}, new double[]{p1[0], p1[1], p2[0], p2[1]}); + distancesWithPoints.put(distance, new double[] {p.x, p.y}); + } + +// Utils.forEach(contour, +// (double[] point) -> { +// double distance = distanceFromPointToMiddleNormal(point, new double[]{p1[0], p1[1], p2[0], p2[1]}); +// distancesWithPoints.put(distance, point); +// } +// ); + + // new pair of points + double[][] pair = new double[2][2]; + + Set distances = distancesWithPoints.keySet(); + pair[0] = distancesWithPoints.remove(Collections.min(distances)); + while(!distancesWithPoints.isEmpty()) { + Double minDistance = Collections.min(distances); + + double[] next = distancesWithPoints.get(minDistance); + if(SimpleGeometry.distance(pair[0], next) > bigDistance) { + pair[1] = next; + return pair; + } + + distancesWithPoints.remove(minDistance); + } + + return pair; + } + + + /** + * Calculate distance from point to middle normal of segment. + * @param p - point of reference. + * @return distance from point to middle normal of segment. + * @tested + */ + private static double distanceFromPointToMiddleNormal(double[] p, double[] line) { + LineFunction func = LineFunctions.getMiddleNormalLineFunction( + new double[]{line[0], line[1]}, new double[]{line[2], line[3]}); + + double A = func.getA(); + double B = func.getB(); + double C = func.getC(); + + double tmp = Math.sqrt(A*A + B*B); + + if(0!=tmp) return Math.abs(A*p[0] + B*p[1] + C) / tmp; + else return SimpleGeometry.distance(p, new double[]{line[0], line[1]}); + } +} + + + + + +/** +* Вернить две точки лежащие наиболее близко к данной линии и нормали справа и слева от нормали соответственно. +* Функция сначала находит список точек претендентов: наиболее близкие к линии точки по pointBufferSize справа и слева. +* После этого для каждой стороны выбирается наиболее близкая точка к нормали. +* @param listOfPoints - списко точек. +* @param line - данная линия. +* @param normal - нормаль. +* @return две точки лежащие наиболее близко к данной линии и нормали справа и слева от нормали соответственно. +*/ +//private static Point[] getTwoPointThatLayOnGivenLine(List listOfPoints, Vector line, Vector normal) { +//short pointBufferSize = 5; +// +//List> leftPoints = new LinkedList>(); +//List> rightPoints = new LinkedList>(); +// +//for(Point point : listOfPoints) { +// double xDiff = (line.x2() - line.x1()); +// double yDiff = (line.y2() -line.y1()); +// +// xDiff = xDiff == 0 ? 0.0001 : xDiff; +// yDiff = yDiff == 0 ? 0.0001 : yDiff; +// +// double closenessToLine = Math.abs((point.x - line.x1())/xDiff - (point.y - line.y1())/yDiff); +// //double closenessToLine = Math.abs((point.x - line.x1())/(line.x2() - line.x1()) - (point.y - line.y1())/(line.y2()-line.y1())); +// +// // position relative to the normal +// // (y1-y2)*x+(x2-x1)*y+(x1*y2-x2*y1) > or < 0 +// double position = (normal.y1()-normal.y2())*point.x + (normal.x2()-normal.x1()) * point.y + (normal.x1()*normal.y2()-normal.x2()*normal.y1()); +// +// Comparator> comparator = new Comparator>() { +// +// @Override +// public int compare(Pair arg0, Pair arg1) { +// if(arg0.getRight() > arg1.getRight()) { +// return 1; +// } +// if(arg0.getRight() < arg1.getRight()) { +// return -1; +// } +// return 0; +// } +// }; +// +// if(position <= 0) { // right position +// if(rightPoints.size() < pointBufferSize && !Double.isNaN(closenessToLine) && Double.isFinite(closenessToLine)) { +// rightPoints.add(new Pair(point, closenessToLine)); +// +// if(rightPoints.size() == pointBufferSize) { +// rightPoints.sort(comparator); +// } +// +// } else { +// for(int j=0; j rightPoint = rightPoints.get(j); +// +// if(closenessToLine < rightPoint.getRight()) { +// rightPoints.add(rightPoints.indexOf(rightPoint), new Pair(point, closenessToLine)); +// rightPoints.remove(rightPoints.size()-1); +// break; +// } +// } +// +// } +// +// } else { // left position +// if(leftPoints.size() < pointBufferSize && !Double.isNaN(closenessToLine) && Double.isFinite(closenessToLine)) { +// leftPoints.add(new Pair(point, closenessToLine)); +// +// if(leftPoints.size() == pointBufferSize) { +// leftPoints.sort(comparator); +// } +// } else { +// for(int j=0; j leftPoint = leftPoints.get(j); +// +// if(closenessToLine < leftPoint.getRight()) { +// leftPoints.add(leftPoints.indexOf(leftPoint), new Pair(point, closenessToLine)); +// leftPoints.remove(leftPoints.size()-1); +// break; +// } +// } +// +// } +// } +//} +// +// +//Point rightPoint = null; +//double rightBestDist = Double.MAX_VALUE; +//for(Pair rightPointPair : rightPoints) { +// Point point = rightPointPair.getLeft(); +// +// double xDiff = (normal.x2() - normal.x1()); +// double yDiff = (normal.y2() - normal.y1()); +// +// xDiff = xDiff == 0 ? 0.0001 : xDiff; +// yDiff = yDiff == 0 ? 0.0001 : yDiff; +// +// double closenessToNormal = Math.abs((point.x - normal.x1())/xDiff - (point.y - normal.y1())/yDiff); +// //double closenessToNormal = Math.abs((point.x - normal.x1())/(normal.x2() - normal.x1()) - (point.y - normal.y1())/(normal.y2()-normal.y1())); +// +// if(closenessToNormal < rightBestDist) { +// rightPoint = point; +// rightBestDist = closenessToNormal; +// } +// +//} +// +//Point leftPoint = null; +//double leftBestDist = Double.MAX_VALUE; +//for(Pair leftPointPair : leftPoints) { +// Point point = leftPointPair.getLeft(); +// +// double xDiff = (normal.x2() - normal.x1()); +// double yDiff = (normal.y2()- normal.y1()); +// +// xDiff = xDiff == 0 ? 0.0001 : xDiff; +// yDiff = yDiff == 0 ? 0.0001 : yDiff; +// +// double closenessToNormal = Math.abs((point.x - normal.x1())/xDiff - (point.y - normal.y1())/yDiff); +// //double closenessToNormal = Math.abs((point.x - normal.x1())/(normal.x2() - normal.x1()) - (point.y - normal.y1())/(normal.y2()-normal.y1())); +// +// if(closenessToNormal < leftBestDist) { +// leftPoint = point; +// leftBestDist = closenessToNormal; +// } +//} +// +//return new Point[]{leftPoint, rightPoint}; +//} + + +/* + * + * public static Point[] getTwoPointThatLayOnGivenLine_(List listOfPoints, Vector line) { + Point point1 = null; + Point point2 = null; + Point prevPoint = listOfPoints.get(0); + + boolean rele = false; + + double minEstimation = Double.MAX_VALUE; + for(Point point : listOfPoints) { + double estimation = Math.abs((point.x - line.x1())/(line.x2() - line.x1()) - (point.y - line.y1())/(line.y2()-line.y1())); + + if(estimation < minEstimation && distance(prevPoint, point)>10) { // ?????? distance it is not good decision + minEstimation= estimation; + + if(rele) { + point1 = point; + } else { + point2 = point; + } + rele = !rele; + prevPoint = point; + } + + } + + return new Point[]{point1, point2}; + } + */ \ No newline at end of file diff --git a/utils/src/main/java/ru/delkom07/geometry/obj/Circle.java b/utils/src/main/java/ru/delkom07/geometry/obj/Circle.java new file mode 100644 index 0000000..35af0c4 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/geometry/obj/Circle.java @@ -0,0 +1,23 @@ +package ru.delkom07.geometry.obj; + +import org.opencv.core.Point; + +public class Circle { + //private double[] center; + private Point center; + private double radius; + + public Circle(Point center, double raduis) { + this.center = center; + this.radius = raduis; + } + + public Point getCenter() { + return center; + } + + public double radius() { + return radius; + } + +} diff --git a/utils/src/main/java/ru/delkom07/geometry/obj/LineFunction.java b/utils/src/main/java/ru/delkom07/geometry/obj/LineFunction.java new file mode 100644 index 0000000..b7fbf1f --- /dev/null +++ b/utils/src/main/java/ru/delkom07/geometry/obj/LineFunction.java @@ -0,0 +1,10 @@ +package ru.delkom07.geometry.obj; + +public interface LineFunction { + double value(double x); + double getA(); + double getB(); + double getC(); + double getk(); + double getb(); +} diff --git a/utils/src/main/java/ru/delkom07/geometry/obj/Vector.java b/utils/src/main/java/ru/delkom07/geometry/obj/Vector.java new file mode 100644 index 0000000..fe2e26c --- /dev/null +++ b/utils/src/main/java/ru/delkom07/geometry/obj/Vector.java @@ -0,0 +1,150 @@ +package ru.delkom07.geometry.obj; + +import org.opencv.core.Mat; +import org.opencv.core.Point; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; + +public class Vector { + private Double x; + private Double y; + + private Double x1; + private Double y1; + private Double x2; + private Double y2; + + boolean coordinateAssigned = false; + + + + public Vector(double x, double y) { + this.x = x; + this.y = y; + + coordinateAssigned = false; + } + + + public Vector(double x1, double y1, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + + this.x = x2-x1; + this.y = y2-y1; + + coordinateAssigned = true; + } + + + public Vector(Point p1, Point p2) { + this.x1 = p1.x; + this.y1 = p1.y; + this.x2 = p2.x; + this.y2 = p2.y; + + this.x = x2-x1; + this.y = y2-y1; + + coordinateAssigned = true; + } + + public Double x() { + return x; + } + + public Double y() { + return y; + } + + public Double x1() { + if(!coordinateAssigned) + throw new RuntimeException("Coordinate not assigned"); + + return x1; + } + + public Double y1() { + if(!coordinateAssigned) + throw new RuntimeException("Coordinate not assigned"); + + return y1; + } + + public Double x2() { + if(!coordinateAssigned) + throw new RuntimeException("Coordinate not assigned"); + + return x2; + } + + public Double y2() { + if(!coordinateAssigned) + throw new RuntimeException("Coordinate not assigned"); + + return y2; + } + + public Point p1() { + return new Point(x1, y1); + } + + public Point p2() { + return new Point(x2, y2); + } + + public void setPoint1(Point p1) { + coordinateAssigned = true; + this.x1 = p1.x; + this.y1 = p1.y; + + this.x = x2-x1; + this.y = y2-y1; + } + + public void setPoint2(Point p2) { + coordinateAssigned = true; + + this.x2 = p2.x; + this.y2 = p2.y; + + this.x = x2-x1; + this.y = y2-y1; + } + + public Double length() { + return Math.sqrt(x*x+y*y); + } + + public Vector normalizedDirection() { + double len = length(); + return new Vector(x/len, y/len); + } + + public Vector inverse() { + return new Vector(x1, y1, x1-x, y1-y); + } + + public Vector deposition(double newX, double newY) { + return new Vector(newX, newY, newX+x, newY+y); + } + + public void draw(Mat img, Scalar color) { + double len = length()/15; + Imgproc.line(img, new Point(x1(), y1()), new Point(x2(), y2()), color, 1); + + double dxL = (-y + x2 + x1)/2.0 - x2(); + double dyL = (x + y2 + y1)/2.0 - y2(); + + double dxR = (y + x2 + x1)/2.0 - x2(); + double dyR = (-x + y2+y1)/2.0 - y2(); + + Imgproc.line(img, new Point(x2(), y2()), new Point(x2 + dxL/len, y2 + dyL/len), color, 1); + Imgproc.line(img, new Point(x2(), y2()), new Point(x2 + dxR/len, y2 + dyR/len), color, 1); + + //Imgproc.line(img, new Point(x2(), y2()), new Point((-y + x2 + x1)/(2.0), (x + y2 + y1)/(2.0)), color, 1); + //Imgproc.line(img, new Point(x2(), y2()), new Point((y + x2 + x1)/(2.0), (-x + y2+y1)/(2.0)), color, 1); + } +} diff --git a/utils/src/main/java/ru/delkom07/util/ArgOption.java b/utils/src/main/java/ru/delkom07/util/ArgOption.java new file mode 100644 index 0000000..5a46c6e --- /dev/null +++ b/utils/src/main/java/ru/delkom07/util/ArgOption.java @@ -0,0 +1,51 @@ +package ru.delkom07.util; + +public class ArgOption { + private String name; + private String shortName; + private String value; + private boolean isNullValue; + + public ArgOption(String name, String shortName, boolean isNullValue) { + this.name = name; + this.shortName = shortName; + this.isNullValue = isNullValue; + } + + void assign(String value) { + this.value = value; + } + + public String getName() { + return name; + } + + public String getShortName() { + return shortName; + } + + public String getValue() { + return value; + } + + public boolean isNullValue() { + return isNullValue; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ArgOption other = (ArgOption) obj; + if (name != other.name) + return false; + if (shortName != other.shortName) { + return false; + } + return true; + } +} diff --git a/utils/src/main/java/ru/delkom07/util/CommandRepresentation.java b/utils/src/main/java/ru/delkom07/util/CommandRepresentation.java new file mode 100644 index 0000000..101d1fe --- /dev/null +++ b/utils/src/main/java/ru/delkom07/util/CommandRepresentation.java @@ -0,0 +1,223 @@ +package ru.delkom07.util; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * Парсер параметров командной строки. + * Формат строки: [--opt1 [--opt2...]] [arg1 [arg2...]] + * Цикл использования: в конструктор передается командная строка, список допустимых опций, + * а так же лимиты по количеству опций и аргументов. + * После: + * - вызов getAcceptedOptions вернет список всех переданных опций. + * - вызовы nextOpt и nextArg вернут следующие переданные по списку опцию и аргумент. + * - вызов flush - позволит сбросить перебор опций и аргументов и начать заново. + * - вызовы getNumOpt и getNumArg возвращают размеры соответствующих списков. + * @author Komyshev + * v.1.0 (B) + */ +public class CommandRepresentation { + // Списки принятых опций и аргументов + private List acceptedOptions = new LinkedList(); + private List acceptedArguments = new ArrayList(); + + // Итераторы для быстрого извлечения опций и аргументов методами nextOpt, nextArg. + private Iterator optionIter; + private Iterator argumentIter; + + /** + * Разбор аргументов и опций командной строки. + * @param args - командная строка. + * @param optList - список названий принимаемых опций. + * @param maxOptNum - максимальное количество опций. + * @param minOptNum - минимальное количество опций. + * @param maxArgNum - максимальное количество аргументов. + * @param minArgNum - минимальносе количество аргументов. + * @throws IncorrectCmdException - исключение некорректной команды. + */ + public CommandRepresentation(String[] args, List optList, int maxOptNum, int minOptNum, int maxArgNum, int minArgNum) throws IncorrectCmdException{ + // Проверка на допустимость команды по количеству аргументов; + if ( (minOptNum + minArgNum > args.length) || + maxOptNum + maxArgNum < args.length){ + throw new IncorrectCmdException("Incorrect count of argument."); + } + + //Получение параметров + for(int i=0; i4) + throw new IncorrectCmdException("Incorrect short option name."); + + ArgOption accepted = getOptionFromList(optList, current, false); + if(null != accepted) { + if(!accepted.isNullValue()) { + if(i!=args.length-1) + accepted.assign(args[++i]); + else + throw new IncorrectCmdException("Option "+ accepted.getName()+ "has not value!"); + } + + acceptedOptions.add(accepted); + } + else + throw new IncorrectCmdException("Incorrect option in command line!"); + } + + + } else { // Параметр - аргумент + acceptedArguments.add(current.trim()); + } + } + + optionIter = acceptedOptions.iterator(); + argumentIter = acceptedArguments.iterator(); + } + + + /** + * Получить список принятых опций. + * @return список принятых опций. + */ + public List getAcceptedOptions() { + return acceptedOptions; + } + + /** + * Получить список принятых аргументов. + * @return список принятых опций. + */ + public List getAcceptedArguments() { + return acceptedArguments; + } + + /** + * Получить список принятых аргументов. + * @return список принятых аргументов. + */ + public String[] getAcceptedArgs() { + String[] acceptedArgs = new String[acceptedArguments.size()]; + for(int i=0; i optList, String option, boolean fullName) { + if(fullName) { + for(ArgOption argOpt : optList) { + if(argOpt.getName().equals(option)) { + return argOpt; + } + } + } else { + for(ArgOption argOpt : optList) { + if(argOpt.getShortName().equals(option)) { + return argOpt; + } + } + } + + return null; + } + +} \ No newline at end of file diff --git a/utils/src/main/java/ru/delkom07/util/CyclicStack.java b/utils/src/main/java/ru/delkom07/util/CyclicStack.java new file mode 100644 index 0000000..1ae8bb9 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/util/CyclicStack.java @@ -0,0 +1,77 @@ +package ru.delkom07.util; + +import java.util.Iterator; + +public class CyclicStack implements Iterable { + private T[] container; + private int pointer = -1; + + + + @SuppressWarnings("unchecked") + public CyclicStack(int size) { + container = (T[]) new Object[size]; + } + + + public void put(T obj) { + pointer++; + if(pointer == container.length) { + pointer = 0; + } + + container[pointer] = obj; + } + + public T pull() { + T obj = container[pointer]; + container[pointer--] = null; + + if(pointer == -1) { + pointer = container.length-1; + } + + return obj; + } + + public T get(int i) { + return container[i]; + } + + + @Override + public Iterator iterator() { + return new CyclicStackIterator(); + } + + + class CyclicStackIterator implements Iterator { + int iPointer = pointer; + int iteration = 0; + + @Override + public boolean hasNext() { + if(iteration < container.length && iPointer != -1 && container[iPointer] != null) { + return true; + } else { + return false; + } + } + + @Override + public T next() { + T obj = container[iPointer--]; + + if(iPointer == -1) { + iPointer = container.length-1; + } + + iteration++; + + return obj; + } + + } + + +} diff --git a/utils/src/main/java/ru/delkom07/util/IncorrectCmdException.java b/utils/src/main/java/ru/delkom07/util/IncorrectCmdException.java new file mode 100644 index 0000000..a9b33d6 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/util/IncorrectCmdException.java @@ -0,0 +1,16 @@ +package ru.delkom07.util; + +/** + * + * @author Komyshev + * v.1.0 (C) + */ +public class IncorrectCmdException extends Exception { + private static final long serialVersionUID = 1L; + + public IncorrectCmdException(){} + + public IncorrectCmdException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/utils/src/main/java/ru/delkom07/util/Pair.java b/utils/src/main/java/ru/delkom07/util/Pair.java new file mode 100644 index 0000000..11c4020 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/util/Pair.java @@ -0,0 +1,33 @@ +package ru.delkom07.util; + +public class Pair { + + private final L left; + private final R right; + + public Pair(L left, R right) { + this.left = left; + this.right = right; + } + + public L getLeft() { return left; } + public R getRight() { return right; } + + @Override + public int hashCode() { return left.hashCode() ^ right.hashCode(); } + + @Override + public boolean equals(Object o) { + + if(o instanceof Pair) { + @SuppressWarnings("unchecked") + Pair pairo = (Pair) o; + + return this.left.equals(pairo.getLeft()) && + this.right.equals(pairo.getRight()); + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/utils/src/main/java/ru/delkom07/workspaces/FileNameReplacer.java b/utils/src/main/java/ru/delkom07/workspaces/FileNameReplacer.java new file mode 100644 index 0000000..8768d10 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/workspaces/FileNameReplacer.java @@ -0,0 +1,5 @@ +package ru.delkom07.workspaces; + +public interface FileNameReplacer { + public String replaceFileName(String fileName); +} diff --git a/utils/src/main/java/ru/delkom07/workspaces/SimpleWorkspace.java b/utils/src/main/java/ru/delkom07/workspaces/SimpleWorkspace.java new file mode 100644 index 0000000..a700142 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/workspaces/SimpleWorkspace.java @@ -0,0 +1,139 @@ +package ru.delkom07.workspaces; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import ru.delkom07.fileutils.FileUtilities; +import ru.delkom07.util.Pair; + +/** + * Простая рабочая среда. + * Создает список операционных единиц (OpUnit) на основе входного/выходного каталогов, + * которые связывают имена и пути входных и выходных файлов текущей сессии выполнения. + * Способен создавать рабочую среду из подкаталогов входного каталога с древовидной структурой. + * Для этого в выходном каталоге создается аналогичная входному каталогу структура подкаталогов и соответствующие файлы в них связываются через OpUnit. + * Выходные файлы при этом не создаются. Вместо этого для них формируются имена и пути (экземпляры класса File). + * В результате каждый OpUnit может обрабатываться независимо друг от друга. + * @author Komyshev + * + */ +public class SimpleWorkspace { + protected File inputDir; // директория входных файлов + protected File outputDir; // директория выходных файлов + + protected List operationUnits = new LinkedList(); // единицы обработки + private boolean treeStrtucture = false; + + + public SimpleWorkspace(File inputDir, File outputDir, boolean treeStructure, FileFilter fileFilter, FileNameReplacer fileNameReplacer) throws IOException { + this.inputDir = inputDir; + this.outputDir = outputDir; + + this.treeStrtucture = treeStructure; + + if(treeStrtucture) { + init(prepareAppropriateTreeOutputs(inputDir, outputDir, fileFilter, fileNameReplacer)); + } else { + init(prepareAppropriateFlatOutputs(inputDir, outputDir, fileFilter, fileNameReplacer)); + } + } + + + /** + * Подготовить список пар файлов (входной-выходной) с соответствующей структурой. + * Для этого считать все входные файлы, соответствующие fileFilter, из директории inputDir а также её поддиректорий, и подготовить пути выходных файлов + * с соответствующей структурой поддиректорий в outputDir. + * @param inputDir - директория входных файлов. + * @param outputDir - директория выходных файлов. + * @param fileFilter - фильтр файлов. + * @return - список пар (входной файл - выходной файл), где путь в поддиректориях в ouputDir выходного файла совпадает с соответствующим путем + * входного файла в поддиректориях inputDir. + * @throws IOException + */ + public static List> prepareAppropriateTreeOutputs(File inputDir, File outputDir, FileFilter fileFilter, FileNameReplacer fileNameReplacer) throws IOException { + List> inputsAndOutputs = new ArrayList>(); + + List inputFiles = FileUtilities.getFilesRecursively(inputDir, fileFilter); + + for(File inputFile : inputFiles) { + File outputImgDir = new File(outputDir, inputFile.getParentFile().getCanonicalPath().replace(inputDir.getCanonicalPath(), "")); + + if(!outputImgDir.exists()) { + outputImgDir.mkdirs(); + } + + File outputImgFile = new File(outputImgDir, (null == fileNameReplacer) ? inputFile.getName() : fileNameReplacer.replaceFileName(inputFile.getName())); + + inputsAndOutputs.add(new Pair(inputFile, outputImgFile)); + } + + return inputsAndOutputs; + } + + + /** + * Подготовить список пар файлов (входной-выходной) без учета поддиректорий. + * @param inputImgsDir - директория входных файлов. + * @param outputImgsDir - директория выходных файлов. + * @param filter - фильтр файлов. + * @param fileNameReplacer - объект, заменяющие (изменяющее) названия входных файлов при создании выходных файлов. + * @return список пар файлов. + */ + public static List> prepareAppropriateFlatOutputs(File inputImgsDir, File outputImgsDir, FileFilter filter, FileNameReplacer fileNameReplacer) { + List> imgPairs = new ArrayList>(); + + File[] files = inputImgsDir.listFiles(filter != null ? filter : FileUtilities.fileFilter); + + // link source image + for(int i=0; i(sourceImgFile, outputImgFile)); + } + + return imgPairs; + } + + + void init(List> insAndOuts) throws IOException { + for(Pair inAndOut : insAndOuts) { + operationUnits.add(new OpUnit(inAndOut.getLeft(), inAndOut.getRight())); + } + } + + + public List getOpUnits() { + return operationUnits; + } + + + /** + * Единица операции. + * @author Komyshev + * + */ + public class OpUnit { + protected File in; + protected File out; + + OpUnit(File in, File out) { + this.in = in; + this.out = out; + } + + public File getIn() { + return in; + } + + public File getOut() { + return out; + } + } +} diff --git a/utils/src/main/java/ru/delkom07/workspaces/StatusWorkspace.java b/utils/src/main/java/ru/delkom07/workspaces/StatusWorkspace.java new file mode 100644 index 0000000..fcaeef9 --- /dev/null +++ b/utils/src/main/java/ru/delkom07/workspaces/StatusWorkspace.java @@ -0,0 +1,110 @@ +package ru.delkom07.workspaces; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.LinkedList; +import java.util.List; + + +/** + * Подкласс класса SimpleWorkspace с дополнительной фукнциональностью - отслеживанием и сохранением статуса обработки операционных единиц. + * @author Komyshev + * + */ +public class StatusWorkspace extends SimpleWorkspace { + private boolean[] statuses; + private File statusFile; + + public StatusWorkspace(File inputDir, File outputDir, boolean treeStructure, FileFilter fileFilter, FileNameReplacer fileNameReplacer) throws IOException { + super(inputDir, outputDir, treeStructure, fileFilter, fileNameReplacer); + + initStatus(); + } + + + private void initStatus() throws IOException { + statuses = new boolean[operationUnits.size()]; // False by default ??? + + statusFile = new File(outputDir, "status.txt"); + + if(!statusFile.exists()) { + statusFile.createNewFile(); + } else { + loadStatus(); + } + } + + private void loadStatus() { + try (BufferedReader br = new BufferedReader(new FileReader(statusFile))) { + String line; + while ((line = br.readLine()) != null) { + setProcessed(line); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + private void setProcessed(String identificationPath) { + boolean success = false; + + for(int i=0; i getRawOpUnits() { + List rawOpUnits = new LinkedList(); + + for(int i=0; i