Add project source code

This commit is contained in:
Ben 2022-01-13 15:55:27 +01:00
parent d935e906b7
commit e01c0fbdd8
Signed by: ben
GPG key ID: 0F54A7ED232D3319
28 changed files with 2038 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

1
.idea/.name Normal file
View file

@ -0,0 +1 @@
computer-vision

View file

@ -0,0 +1,88 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<option name="LAYOUT_STATIC_IMPORTS_SEPARATELY" value="false" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="java" withSubpackages="true" static="false" />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="net.minecraft" withSubpackages="true" static="false" />
<emptyLine />
<package name="net.minecraftforge" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="logisticspipes" withSubpackages="true" static="false" />
<package name="network.rs485" withSubpackages="true" static="false" />
</value>
</option>
<option name="FORMATTER_TAGS_ENABLED" value="true" />
<JavaCodeStyleSettings>
<option name="LAYOUT_STATIC_IMPORTS_SEPARATELY" value="false" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="java" withSubpackages="true" static="false" />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="net.minecraft" withSubpackages="true" static="false" />
<emptyLine />
<package name="net.minecraftforge" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="logisticspipes" withSubpackages="true" static="false" />
<package name="network.rs485" withSubpackages="true" static="false" />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
<option name="BLANK_LINES_AFTER_ANONYMOUS_CLASS_HEADER" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_RESOURCES" value="false" />
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="RESOURCE_LIST_WRAP" value="5" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_LIST_WRAP" value="1" />
<option name="EXTENDS_KEYWORD_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="5" />
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="VARIABLE_ANNOTATION_WRAP" value="2" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

6
.idea/compiler.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

18
.idea/gradle.xml Normal file
View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="/usr/share/java/gradle" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

20
.idea/jarRepositories.xml Normal file
View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="libraries-with-intellij-classes">
<option name="intellijApiContainingLibraries">
<list>
<LibraryCoordinatesState>
<option name="artifactId" value="ideaIU" />
<option name="groupId" value="com.jetbrains.intellij.idea" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="ideaIU" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="ideaIC" />
<option name="groupId" value="com.jetbrains.intellij.idea" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="ideaIC" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="pycharmPY" />
<option name="groupId" value="com.jetbrains.intellij.pycharm" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="pycharmPY" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="pycharmPC" />
<option name="groupId" value="com.jetbrains.intellij.pycharm" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="pycharmPC" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="clion" />
<option name="groupId" value="com.jetbrains.intellij.clion" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="clion" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="riderRD" />
<option name="groupId" value="com.jetbrains.intellij.rider" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="riderRD" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="goland" />
<option name="groupId" value="com.jetbrains.intellij.goland" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="goland" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
</list>
</option>
</component>
</project>

10
.idea/misc.xml Normal file
View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

124
.idea/uiDesigner.xml Normal file
View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

35
build.gradle.kts Normal file
View file

@ -0,0 +1,35 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.6.10"
application
}
group = "network.rs485.ben"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.0")
implementation("org.jetbrains.kotlin:kotlin-annotations-jvm:1.6.10")
implementation("io.ktor:ktor-io:1.6.7")
implementation(files("/usr/share/java/opencv.jar")) // provided by the OS (Arch Linux) after installation
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
tasks.withType<KotlinCompile>() {
kotlinOptions.jvmTarget = "17"
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
application {
mainClass.set("network.rs485.ben.computervision.MainKt")
}

1
gradle.properties Normal file
View file

@ -0,0 +1 @@
kotlin.code.style=official

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or 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 UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$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 "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# 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
;;
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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View file

@ -0,0 +1,89 @@
@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%" == "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%"=="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!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

3
settings.gradle.kts Normal file
View file

@ -0,0 +1,3 @@
rootProject.name = "computer-vision"

View file

@ -0,0 +1,37 @@
package network.rs485.ben.computervision
import network.rs485.ben.computervision.CvContext.withMat
import org.opencv.core.CvType
import org.opencv.core.Mat
import org.opencv.core.Scalar
import org.opencv.imgproc.Imgproc
internal val COLOR_BLACK = Scalar(0.0, 0.0, 0.0, 255.0)
internal val COLOR_WHITE = Scalar(255.0, 255.0, 255.0, 255.0)
internal val COLOR_RED = Scalar(0.0, 0.0, 180.0, 255.0)
internal val COLOR_GREEN = Scalar(0.0, 255.0, 0.0, 255.0)
internal val COLOR_BLUE = Scalar(255.0, 0.0, 0.0, 255.0)
internal val COLOR_YELLOW = Scalar(0.0, 240.0, 255.0, 255.0)
internal operator fun Scalar.plus(other: Scalar): Scalar =
Scalar((0..3).map { `val`[it] + other.`val`[it] }.toDoubleArray())
internal operator fun Scalar.minus(other: Scalar): Scalar =
Scalar((0..3).map { `val`[it] - other.`val`[it] }.toDoubleArray())
internal operator fun Scalar.times(factor: Double): Scalar =
Scalar((0..3).map { `val`[it] * factor }.toDoubleArray())
internal operator fun Scalar.div(divider: Double): Scalar =
Scalar((0..3).map { (`val`[it] / divider) }.toDoubleArray())
internal fun Scalar.asArray(length: Int = 4): DoubleArray {
assert(length > 0)
return (0 until length).map { `val`.getOrElse(it) { 0.0 } }.toDoubleArray()
}
internal fun Mat.copyToColor(image: Mat) = withMat(MatInfo.from(image, CvType.CV_8U)) { tmp ->
convertTo(tmp, CvType.CV_8U)
Imgproc.cvtColor(tmp, image, Imgproc.COLOR_GRAY2BGR)
image
}

View file

@ -0,0 +1,110 @@
package network.rs485.ben.computervision
import io.ktor.utils.io.pool.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.opencv.core.*
import org.opencv.imgcodecs.Imgcodecs
import org.opencv.imgproc.Imgproc
import java.nio.file.Path
data class MatInfo(val rows: Int, val cols: Int, val type: Int) {
companion object {
fun from(mat: Mat) = MatInfo(mat.rows(), mat.cols(), mat.type())
fun from(mat: Mat, type: Int) = MatInfo(mat.rows(), mat.cols(), type)
}
fun create(): Mat = Mat(rows, cols, type)
}
private val histMatInfo = MatInfo(256, 1, CvType.CV_32FC1)
private val predefinedChannels = listOf(16, 24, 64, 128)
private val matPoolsToCreate = listOf(
MatInfo(32, 32, CvType.CV_8UC1) to Runtime.getRuntime().availableProcessors() * 4,
histMatInfo to Runtime.getRuntime().availableProcessors() * 4,
) + predefinedChannels.map {
MatInfo(it, it, CvType.CV_8UC1) to Runtime.getRuntime().availableProcessors() * 2
}
object CvContext {
lateinit var gaussianKernel: Mat
lateinit var histChannelPar: MatOfInt
lateinit var histSizePar: MatOfInt
lateinit var histRangesPar: MatOfFloat
lateinit var matPools: HashMap<MatInfo, MatPool>
fun initialize() {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME)
this.gaussianKernel = Imgproc.getGaussianKernel(GAUSSIAN_KERNEL_SIZE, -1.0)
this.histChannelPar = MatOfInt(0)
this.histSizePar = MatOfInt(HIST_SIZE)
this.histRangesPar = MatOfFloat(0f, 255f)
this.matPools = matPoolsToCreate.associateTo(HashMap()) { (matInfo, capacity) ->
matInfo to MatPool(matInfo, capacity)
}
}
internal inline fun <T> withGrayCalcHist(image: Mat, mask: Mat, block: (Mat) -> T): T = withMat(histMatInfo) { hist ->
Imgproc.calcHist(
/* images = */ listOf(image),
/* channels = */ histChannelPar,
/* mask = */ mask,
/* hist = */ hist,
/* histSize = */ histSizePar,
/* ranges = */ histRangesPar,
)
block(hist)
}
internal inline fun <T> withRotationMatrix(angle: Double, center: Point, block: (Mat) -> T): T =
Imgproc.getRotationMatrix2D(
/* center = */ center,
/* angle = */ Math.toDegrees(angle),
/* scale = */ 1.0,
).letAutoclean(block)
class MatPool(
private val info: MatInfo,
capacity: Int,
) : DefaultPool<Mat>(capacity) {
override fun produceInstance(): Mat = info.create()
}
internal inline fun <T> withMat(matInfo: MatInfo, block: (Mat) -> T): T {
val upperMatInfo = if (matInfo.type == CvType.CV_8UC1) {
predefinedChannels.find { matInfo.rows <= it && matInfo.cols <= it }
?.let { MatInfo(it, it, CvType.CV_8UC1) }
?: matInfo
} else matInfo
return if (matPools.containsKey(upperMatInfo)) {
val pool = matPools[upperMatInfo]!!
val mat = pool.borrow()
autoclean(mat, { pool.recycle(mat) }, block)
} else {
println("Not pooled: $matInfo")
matInfo.create().letAutoclean(block)
}
}
internal fun recycle(image: Mat) = matPools[MatInfo.from(image)]?.recycle(image) ?: image.release()
internal suspend fun readAndConvertImage(imagePath: Path): Pair<Mat, Mat> {
val displayImage = withContext(Dispatchers.IO) {
Imgcodecs.imread(imagePath.toString())
} ?: throw RuntimeException("Could not load image from $imagePath")
println("Loaded image $imagePath as $displayImage")
val matInfo = MatInfo(displayImage.rows(), displayImage.cols(), CvType.CV_8UC1)
val image = matPools.computeIfAbsent(matInfo) {
MatPool(matInfo, Runtime.getRuntime().availableProcessors())
}.borrow()
Imgproc.cvtColor(displayImage, image, Imgproc.COLOR_BGR2GRAY)
return displayImage to image
}
}
internal inline fun <V, T> autoclean(obj: V, release: () -> Unit, block: (V) -> T): T =
runCatching { block(obj) }.also { release() }.getOrThrow()
internal inline fun <T> Mat.letAutoclean(block: (Mat) -> T): T =
autoclean(this, this::release, block)
internal inline fun <T> withTmpMat(block: (tmp: Mat) -> T): T = Mat().letAutoclean(block)

View file

@ -0,0 +1,24 @@
package network.rs485.ben.computervision
import org.opencv.core.Mat
interface DimensionProvider {
val minX: Int
val maxX: Int
val minY: Int
val maxY: Int
}
class CroppingDimensionProvider(lines: List<Line>) : DimensionProvider {
override val minX: Int = lines.minOf { it.first.x }
override val maxX: Int = lines.maxOf { it.second.x }
override val minY: Int = lines.first().first.y
override val maxY: Int = lines.last().first.y
}
class ImageDimensionProvider(image: Mat) : DimensionProvider {
override val minX: Int = 0
override val maxX: Int = image.cols() - 1
override val minY: Int = 0
override val maxY: Int = image.rows() - 1
}

View file

@ -0,0 +1,73 @@
package network.rs485.ben.computervision
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.opencv.core.Mat
import org.opencv.core.Scalar
import java.util.*
class Entity {
val lines: TreeSet<Line> = sortedSetOf(comparator = LineComparator)
fun color(color: Scalar, image: Mat) {
lines.forEach {
image.drawLine(it, color)
}
}
fun merge(other: Entity) {
lines.addAll(other.lines)
}
override fun equals(other: Any?): Boolean {
return this === other || other is Entity && lines == other.lines
}
override fun hashCode(): Int {
return lines.hashCode()
}
override fun toString(): String {
return "Entity(${lines.joinToString(limit = 3) { "${it.first} -- ${it.second}" }})"
}
}
fun MutableList<Entity>.merge(entity: Entity) {
val toMerge = mutableSetOf<Entity>()
entity.lines.forEach { line ->
toMerge.addAll(this.filter { otherEntity ->
otherEntity.lines.any(line::intersects)
})
}
if (toMerge.size == 0) {
add(entity)
} else {
val retainedEntity = toMerge.first()
retainedEntity.merge(entity)
toMerge.forEach {
if (it !== retainedEntity) {
this.remove(it)
retainedEntity.merge(it)
}
}
}
}
suspend fun MutableList<Entity>.colorRanged(
start: Scalar,
end: Scalar,
image: Mat,
windowMgr: IWindowManager,
) = coroutineScope {
if (size == 0) return@coroutineScope
if (size == 1) get(0).color(start, image)
val direction = (end - start) * (1.0 / (size - 1))
forEachIndexed { index, entity ->
entity.color(start + (direction * index.toDouble()), image)
launch {
windowMgr.updateImage(image)
}
if (ANIMATION_DELAY.isPositive()) delay(ANIMATION_DELAY)
}
}

View file

@ -0,0 +1,194 @@
package network.rs485.ben.computervision
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.opencv.core.Mat
class EntityScanner(private val animate: Boolean, private val scanBarSpacing: Int, private val foregroundThreshold: Float) {
private fun Mat.isValid(row: Int, column: Int): Boolean =
(this[row, column]?.getOrNull(0) ?: 255.0) < foregroundThreshold
private fun Mat.isValid(point: Pixel): Boolean =
(this[point]?.getOrNull(0) ?: 255.0) < foregroundThreshold
suspend fun scanImage(image: Mat, displayImage: Mat, windowMgr: IWindowManager) = coroutineScope {
val entities = mutableListOf<Entity>()
val mergeMutex = Mutex()
lineScan(image, displayImage, windowMgr)
.map { line ->
lineGrowToEntityDown(line, image, displayImage, windowMgr).also { entity ->
// add missing upper parts of all entities
lineGrowToEntityUp(line, image, displayImage, windowMgr, entity)
}
}
.collect {
mergeMutex.withLock {
entities.merge(it)
}
}
entities.removeIf { it.lines.size < 6 }
if (animate) {
entities.colorRanged(COLOR_GREEN, COLOR_RED, displayImage, windowMgr)
windowMgr.updateImage(displayImage)
}
return@coroutineScope entities
}
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun lineScan(
image: Mat,
displayImage: Mat,
windowMgr: IWindowManager,
) = channelFlow {
coroutineScope {
for (i in 0 until image.height().div(scanBarSpacing)) {
launch {
val lineNumber = i * scanBarSpacing
var start = Pixel(0, lineNumber)
var end = Pixel(image.width() - 1, lineNumber)
if (animate) {
displayImage.drawLine(Line(start, end), COLOR_RED)
launch {
windowMgr.updateImage(displayImage)
}
}
var onEntity = false
for (x in 0 until image.width()) {
val color = if (image.isValid(lineNumber, x)) {
COLOR_GREEN.also {
if (!onEntity) {
onEntity = true
start = Pixel(x, start.y)
}
}
} else {
COLOR_BLUE.also {
if (onEntity) {
onEntity = false
send(start to Pixel(x, start.y))
}
}
}
end = Pixel(x, end.y)
if (animate) {
// color the current dot on the line
displayImage.put(lineNumber, x, *color.asArray(3))
launch {
windowMgr.updateImage(displayImage)
}
if (ANIMATION_DELAY.isPositive()) delay(ANIMATION_DELAY)
}
}
if (onEntity) {
send(start to Pixel(image.width() - 1, start.y))
}
}
}
}
}
private suspend fun lineGrowToEntityDown(
line: Line,
image: Mat,
displayImage: Mat,
windowMgr: IWindowManager,
entity: Entity = Entity(),
) = lineGrowToEntity(
line = line,
image = image,
displayImage = displayImage,
windowMgr = windowMgr,
entity = entity,
last = (line.first.y + scanBarSpacing - 1).coerceIn(0, image.height() - 1),
step = 1,
)
private suspend fun lineGrowToEntityUp(
line: Line,
image: Mat,
displayImage: Mat,
windowMgr: IWindowManager,
entity: Entity = Entity(),
) = lineGrowToEntity(
line = line,
image = image,
displayImage = displayImage,
windowMgr = windowMgr,
entity = entity,
last = (line.first.y - scanBarSpacing + 1).coerceIn(0, image.height() - 1),
step = -1,
)
private suspend fun lineGrowToEntity(
line: Line,
image: Mat,
displayImage: Mat,
windowMgr: IWindowManager,
entity: Entity,
last: Int,
step: Int,
): Entity = coroutineScope {
assert(step != 0)
suspend fun lineFound(foundLine: Line) {
assert(foundLine.first.x >= 0)
assert(foundLine.first.y >= 0)
assert(foundLine.first.x <= foundLine.second.x)
assert(foundLine.first.y == foundLine.second.y)
entity.lines.add(foundLine)
if (animate) {
displayImage.drawLine(foundLine, COLOR_YELLOW)
launch {
windowMgr.updateImage(displayImage)
}
if (ANIMATION_DELAY.isPositive()) delay(ANIMATION_DELAY)
}
}
lineFound(line)
suspend fun lineGrowRecursion(foundLine: Line) {
lineFound(foundLine)
if (step > 0 && foundLine.first.y <= last || step < 0 && foundLine.first.y >= last) {
lineGrowToEntity(
line = foundLine,
image = image,
displayImage = displayImage,
windowMgr = windowMgr,
entity = entity,
last = last,
step = step,
)
}
}
var start = Pixel(line.first.x, line.first.y + step)
if (start.y < 0 || start.y >= image.height()) return@coroutineScope entity
val row = start.y
fun IntProgression.lastValid() = takeWhile { image.isValid(row, it) }.last()
var end = start
if (image.isValid(start)) {
start = Pixel(line.first.x.downTo(0).lastValid(), start.y)
end = Pixel(line.first.x.until(image.width()).lastValid(), end.y)
lineGrowRecursion(start to end)
}
var next = end.x + 2
while (next <= line.second.x) {
if (image.isValid(row, next)) {
start = Pixel(end.x + 2, start.y)
end = Pixel(next.until(image.width()).lastValid(), start.y)
lineGrowRecursion(start to end)
next = end.x + 2
} else {
++next
}
}
return@coroutineScope entity
}
}

View file

@ -0,0 +1,120 @@
package network.rs485.ben.computervision
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import java.nio.file.Path
import kotlin.math.roundToInt
internal const val DISTANCE_DEVIATION: Double = 1.0
internal const val DISTANCE_STEP = 0.5
internal const val DILATION_SIZE = 1.0
fun interface DeviationFunction {
fun calculate(level: Int, value: Double): Double
}
data class DeviationParameters(val deviationMin: Int, val deviationFunc: DeviationFunction, val maximumDeviation: Double) {
val deviationRange = deviationMin until HIST_SIZE
}
class Golden(
val goldenName: String,
val goldenColor: Scalar,
entity: Entity,
val image: Mat,
val displayImage: Mat,
val windowManager: IWindowManager,
val deviationParameters: DeviationParameters,
) : IdentifiableEntity(entity, ImageDimensionProvider(image)) {
companion object {
suspend fun createGolden(
parentScope: CoroutineScope,
scanner: EntityScanner,
goldenName: String,
goldenPath: Path,
goldenColor: Scalar,
deviationParameters: DeviationParameters,
): Golden {
val windowMgr = WINDOW_MANAGER_FACTORY.createWindowManager(goldenName)
val (displayImage, image) = CvContext.readAndConvertImage(goldenPath)
parentScope.launch {
windowMgr.display(displayImage)
}
windowMgr.setDimension(256, 256)
val entity =
scanner.scanImage(image, displayImage, windowMgr)
.takeIf { it.size == 1 }
?.get(0)
?: throw RuntimeException("Cannot find $goldenName in golden image")
val golden = Golden(
goldenName = goldenName,
goldenColor = goldenColor,
entity = entity,
image = image,
displayImage = displayImage,
windowManager = windowMgr,
deviationParameters = deviationParameters,
)
golden.distanceMat.copyToColor(displayImage)
parentScope.launch {
golden.updateImage()
}
return golden
}
}
val orientation: Line = run {
val midX = minMaxDistance.maxLoc.x.roundToInt()
val midY = minMaxDistance.maxLoc.y.roundToInt()
val midRounded = Pixel(midX, midY)
if (minMaxDistance.maxLoc != midRounded.vec2D()) println("${minMaxDistance.maxLoc} != ${midRounded.vec2D()}")
transformedLines.filter { it.first.y >= midY }
.flatMap { (first, second) -> listOf(first, second) }
.map { startPixel ->
val endPixel = entityMask.searchGreatest(
center = midRounded,
direction = (midRounded - startPixel).vec2D().normalize(),
searchRange = (0..(cols + rows)) // use distance step?
) ?: throw RuntimeException(
"Could not find any border for $goldenName center $midRounded in $entityMask:\n" +
entityMask.dump()
)
startPixel to endPixel
}
.maxByOrNull { it.vec2D().magnitude }
?: throw IllegalStateException("Could not find orientation for $goldenName")
}
val orientationRadius = orientation.vec2D().magnitude / 2.0
val entityMaskDilated: Mat = Mat(rows, cols, entityMask.type()).also { dest ->
val dilationKernel = Imgproc.getStructuringElement(
/* shape = */ Imgproc.MORPH_ELLIPSE,
/* ksize = */ Size(2 * DILATION_SIZE + 1, 2 * DILATION_SIZE + 1),
)
Imgproc.dilate(entityMask, dest, dilationKernel)
}
internal inline fun <T> withCentrumMask(identity: IdentifiableEntity, block: (Mat) -> T): T =
CvContext.withMat(MatInfo(identity.rows, identity.cols, CvType.CV_8UC1)) { maskMat ->
Core.inRange(
/* src = */ identity.distanceMat,
/* lowerb = */ Scalar(minMaxDistance.maxVal.minus(DISTANCE_DEVIATION)),
/* upperb = */ Scalar(minMaxDistance.maxVal.plus(DISTANCE_DEVIATION)),
/* dst = */ maskMat,
)
block(maskMat)
}
suspend fun updateImage() = windowManager.updateImage(displayImage)
override fun release() {
super.release()
entityMaskDilated.release()
windowManager.dispose()
}
override fun toString(): String = "Golden($goldenName)"
}

View file

@ -0,0 +1,55 @@
package network.rs485.ben.computervision
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
open class IdentifiableEntity(
entity: Entity,
private val dimensionProvider: DimensionProvider = CroppingDimensionProvider(entity.lines.toList()),
) {
companion object {
const val DIST_TRANSFORM_MASK_SIZE: Int = 3
// DIST_L2 is the simple euclidean distances
const val DIST_TRANSFORM_FUNC = Imgproc.DIST_L2
}
val rows: Int = dimensionProvider.maxY - dimensionProvider.minY + 1
val cols: Int = dimensionProvider.maxX - dimensionProvider.minX + 1
val rect: Rect
get() = Rect(dimensionProvider.minX, dimensionProvider.minY, cols, rows)
val lines = entity.lines.toList()
val transformedLines = lines.map {
transformPixel(it.first) to transformPixel(it.second)
}
val entityMask: Mat = Mat(rows, cols, CvType.CV_8UC1).also { maskMat ->
maskMat.setTo(COLOR_BLACK)
transformedColor(COLOR_WHITE, maskMat)
}
val distanceMat: Mat = Mat(rows, cols, CvType.CV_32FC1).also { distanceMat ->
Imgproc.distanceTransform(entityMask, distanceMat, DIST_TRANSFORM_FUNC, DIST_TRANSFORM_MASK_SIZE)
}
val minMaxDistance: Core.MinMaxLocResult by lazy { Core.minMaxLoc(distanceMat, entityMask) }
fun transformedColor(color: Scalar, image: Mat) {
transformedLines.forEach {
image.drawLine(it, color)
}
}
fun transformPixel(pixel: Pixel) = Pixel(pixel.x - dimensionProvider.minX, pixel.y - dimensionProvider.minY)
open fun release() {
entityMask.release()
distanceMat.release()
}
override fun toString(): String {
return "IdentifiableEntity($rect)"
}
}
internal inline fun <T> withIdEntity(entity: Entity, block: (identity: IdentifiableEntity) -> T): T =
IdentifiableEntity(entity).let { identity ->
autoclean(identity, identity::release, block)
}

View file

@ -0,0 +1,111 @@
package network.rs485.ben.computervision
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sqrt
internal typealias Pixel = Pair<Int, Int>
internal typealias Line = Pair<Pixel, Pixel>
internal val Pixel.x: Int
get() = first
internal val Pixel.y: Int
get() = second
internal operator fun Pixel.plus(other: Pixel): Pixel = Pixel(x + other.x, y + other.y)
internal operator fun Pixel.minus(other: Pixel): Pixel = Pixel(x - other.x, y - other.y)
internal operator fun Mat.get(pixel: Pixel): DoubleArray? =
get(/* row = */ pixel.y, /* col = */ pixel.x)
fun Point.rounded(): Pixel = x.roundToInt() to y.roundToInt()
internal operator fun Mat.get(point: Point): DoubleArray? =
get(/* row = */ point.y.toInt(), /* col = */ point.x.toInt())
class Vector2D(x: Double, y: Double) : Point(x, y) {
val magnitude: Double
get() = sqrt(x.pow(2.0) + y.pow(2.0))
constructor(other: Point) : this(other.x, other.y)
fun normalize() = apply {
magnitude.let {
x /= it
y /= it
}
}
operator fun times(factor: Double): Vector2D = Vector2D(x * factor, y * factor)
operator fun div(divider: Double): Vector2D = Vector2D(x / divider, y / divider)
operator fun plus(point: Point) = Vector2D(x + point.x, y + point.y)
}
@JvmName("pixelToVec2D")
internal fun Pixel.vec2D(): Vector2D = Vector2D(x.toDouble(), y.toDouble())
@JvmName("lineToVec2D")
internal fun Line.vec2D(): Vector2D = Vector2D((second.x - first.x).toDouble(), (second.y - first.y).toDouble())
internal object LineComparator : Comparator<Line> {
override fun compare(line1: Line, line2: Line): Int {
assert(line1.first.y == line1.second.y)
assert(line2.first.y == line2.second.y)
val rowCompare = line1.first.y.compareTo(line2.first.y)
if (rowCompare != 0) return rowCompare
val lineStartCompare = line1.first.x.compareTo(line2.first.x)
if (lineStartCompare != 0) return lineStartCompare
return line1.second.x.compareTo(line2.second.x)
}
}
internal fun Line.intersects(other: Line): Boolean {
assert(first.y == second.y)
assert(other.first.y == other.second.y)
if (first.y != other.first.y) return false
if (first.x == other.first.x) return true
return if (first.x < other.first.x) {
second.x >= other.first.x
} else {
other.second.x >= first.x
}
}
internal fun Mat.drawLine(line: Line, color: Scalar) = Imgproc.line(
/* img = */ this,
/* pt1 = */ line.first.vec2D(),
/* pt2 = */ line.second.vec2D(),
/* color = */ color,
)
internal fun Mat.filterPixels(filterFunc: (value: DoubleArray) -> Boolean): List<Pixel> =
(0 until rows()).flatMap { row ->
(0 until cols())
.filter { col -> filterFunc(get(row, col)) }
.map { col -> Pixel(col, row) }
}
internal fun Mat.sumOfHist(
indices: IntRange,
maximum: Double,
mapFunc: (level: Int, value: Double) -> Double,
): Double {
var sum: Double = 0.toDouble()
for (idx in indices) {
sum += mapFunc(idx, get(idx, 0)[0])
if (sum > maximum) break
}
return sum
}
fun <T : Number> Mat.searchGreatest(center: Pixel, direction: Vector2D, searchRange: Iterable<T>): Pixel? =
searchRange.map { factor: Number -> (direction * factor.toDouble()).rounded() + center }
.filter { it.x in (0 until width()) && it.y in (0 until height()) }
.lastOrNull { pixel -> get(pixel)?.let { it[0] > 0 } ?: false }

View file

@ -0,0 +1,129 @@
package network.rs485.ben.computervision
import kotlinx.coroutines.*
import org.opencv.core.Mat
import org.opencv.highgui.HighGui
import org.opencv.highgui.ImageWindow
import java.awt.event.KeyEvent
import java.awt.event.KeyListener
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.awt.image.BufferedImage
import java.util.concurrent.atomic.AtomicInteger
import javax.swing.ImageIcon
import javax.swing.JLabel
import kotlin.system.exitProcess
val DEFAULT_WINDOW_MANAGER_FACTORY = WindowManagerFactory(::WindowManager)
val NOOP_WINDOW_MANAGER_FACTORY = WindowManagerFactory { NoopWindowManager }
fun interface WindowManagerFactory {
fun createWindowManager(windowName: String): IWindowManager
}
interface IWindowManager {
suspend fun display(img: Mat)
suspend fun updateImage(img: Mat)
fun dispose()
fun setDimension(width: Int, height: Int)
}
private object NoopWindowManager : IWindowManager {
override suspend fun display(img: Mat) = Unit
override suspend fun updateImage(img: Mat) = Unit
override fun dispose() = Unit
override fun setDimension(width: Int, height: Int) = Unit
}
private class WindowManager(windowName: String) : IWindowManager {
private var updateState: AtomicInteger = AtomicInteger(2)
private var disposeDeferred: CompletableDeferred<Unit>? = null
private val window: ImageWindow = ImageWindow(windowName, 0)
override suspend fun display(img: Mat) = withContext(Dispatchers.Main) {
if (disposeDeferred != null) {
throw IllegalStateException("A window is already shown")
}
window.setMat(img)
window.show()
updateState.set(0)
val deferred = CompletableDeferred<Unit>()
disposeDeferred = deferred
try {
window.frame.addWindowListener(object : WindowAdapter() {
override fun windowClosed(e: WindowEvent?) {
deferred.cancel("Window closed")
}
})
window.frame.addKeyListener(object : KeyListener {
override fun keyTyped(e: KeyEvent) {}
override fun keyPressed(e: KeyEvent) {}
override fun keyReleased(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_ESCAPE) {
deferred.cancel("Escape pressed")
}
}
})
deferred.await()
} finally {
disposeDeferred = null
window.dispose()
}
}
override suspend fun updateImage(img: Mat) {
window.setMat(img)
val state = updateState.getAndUpdate { it.plus(1).coerceAtMost(3) }
if (state == 3) return
withContext(Dispatchers.Main) {
updateState.updateAndGet { it.minus(1).coerceAtLeast(0) }
window.show()
}
}
override fun dispose() {
disposeDeferred?.complete(Unit)
}
override fun setDimension(width: Int, height: Int) = window.setNewDimension(width, height)
/**
* @see org.opencv.highgui.HighGui.waitKey(Int)
*/
private fun ImageWindow.show() {
if (img != null) {
val bufferedImage = try {
HighGui.toBufferedImage(img)
} catch (error: UnsupportedOperationException) {
System.err.println("Cannot show image ($img) in window ${window.name}: ${error.message}")
BufferedImage(img.width(), img.height(), BufferedImage.TYPE_INT_RGB).also {
for (y in 0 until it.height) {
if (y % 2 == 1) {
for (x in 0 until it.width) {
it.setRGB(x, y, 0xff00ff)
}
}
}
}
}
val imageIcon = ImageIcon(bufferedImage)
if (lbl == null) {
val frame = HighGui.createJFrame(name, flag)
val lbl = JLabel(imageIcon)
setFrameLabelVisible(frame, lbl)
} else {
lbl.icon = imageIcon
}
} else {
System.err.println("Error: no imshow associated with namedWindow: \"$name\"")
exitProcess(-1)
}
}
private fun ImageWindow.dispose() {
frame.dispose()
}
}

View file

@ -0,0 +1,481 @@
@file:OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
package network.rs485.ben.computervision
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.flow.*
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import java.io.FileNotFoundException
import java.nio.file.FileSystemNotFoundException
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.*
import kotlin.math.acos
import kotlin.system.exitProcess
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
internal const val HIST_SIZE = 256
internal const val FOREGROUND_THRESHOLD = 120.0f
internal const val GAUSSIAN_KERNEL_SIZE = 3
internal val COROUTINE_BUFFER_SIZE = System.getProperties().getProperty("cv.coroutines.buffer.size", "4").toInt()
internal val MAXIMUM_PARALLEL_IMAGES = System.getProperties().getProperty("cv.images.buffer.size", "1").toInt()
//internal val ANIMATION_DELAY: Duration = 100.milliseconds
internal val ANIMATION_DELAY: Duration = Duration.ZERO
//internal val WINDOW_MANAGER_FACTORY = DEFAULT_WINDOW_MANAGER_FACTORY
internal val WINDOW_MANAGER_FACTORY = NOOP_WINDOW_MANAGER_FACTORY
private val SINGLE_DISPATCHER = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
internal object Resource {
fun getPath(name: String): Path {
val path = try {
javaClass.getResource("/$name")?.toURI()?.let(Paths::get)
} catch (error: FileSystemNotFoundException) {
System.err.println("Could not read resource $name, error:\n $error\n using fallback..")
null
} ?: Paths.get(name).apply {
if (!exists()) {
throw FileNotFoundException("Cannot find $this on filesystem $fileSystem")
}
}
return path
}
}
typealias Candidate = Triple<Pixel, Vector2D, Double>
private suspend fun findGoldenPoI(
image: Mat,
identity: IdentifiableEntity,
golden: Golden,
centrumMask: Mat,
): List<Pair<Golden, Pair<Pixel, Candidate>>> {
val goldenOrientation = golden.orientation.vec2D()
val goldenDirection = Vector2D(goldenOrientation).normalize()
val searchMag = golden.orientationRadius
.let { (it - DISTANCE_DEVIATION..it + DISTANCE_DEVIATION) }
val bestCenters = channelFlow bestCenters@{
for (center in centrumMask.filterPixels { it[0] > 0.0 }) {
launch findCenterDeviation@{
val intoGoldSpace = golden.minMaxDistance.maxLoc.rounded() - center
val rect = golden.rect.apply {
x += identity.rect.x - intoGoldSpace.x
y += identity.rect.y - intoGoldSpace.y
}
if (
rect.x < 0 ||
rect.y < 0 ||
rect.x + rect.width >= image.width() ||
rect.y + rect.height >= image.height()
) {
if (ANIMATION_DELAY.isPositive()) println("Not considering $center for $identity on $golden")
return@findCenterDeviation
}
image.withSubmatSafe(rect) { subImage ->
val deviationFlow = channelFlow {
suspend fun ProducerScope<Candidate>.emitDeviation(startPixel: Pixel) {
val centerStart = center - startPixel
if (centerStart == Pixel(0, 0)) return
val dirVec = centerStart.vec2D().normalize()
val endpix = try {
identity.entityMask.searchGreatest(
center = center,
direction = dirVec,
searchRange = searchMag.progression(step = DISTANCE_STEP)
)
} catch (error: IllegalArgumentException) {
System.err.println(
"Error when finding greatest magnitude of $center for $identity.\n" +
" Values: center = $center, dir = $dirVec, searchMag = $searchMag\n" +
" $error"
)
null
} ?: return
val angle = acos(dirVec.dot(goldenDirection))
val deviation = CvContext.withMat(MatInfo.from(subImage)) { tmp ->
subImage.copyTo(tmp)
CvContext.withRotationMatrix(
angle = angle,
center = golden.minMaxDistance.maxLoc,
) { intoGoldSpaceRot ->
Imgproc.warpAffine(tmp, tmp, intoGoldSpaceRot, tmp.size())
}
Core.absdiff(golden.image, tmp, tmp)
Imgproc.filter2D(tmp, tmp, -1, CvContext.gaussianKernel)
CvContext.withGrayCalcHist(
image = tmp,
mask = golden.entityMaskDilated,
) { hist ->
hist.sumOfHist(
indices = golden.deviationParameters.deviationRange,
maximum = golden.deviationParameters.maximumDeviation,
mapFunc = golden.deviationParameters.deviationFunc::calculate,
)
}
}
if (deviation <= golden.deviationParameters.maximumDeviation) {
send(Candidate(endpix, dirVec, deviation))
}
}
identity.transformedLines.forEach {
launch {
emitDeviation(it.first)
}
launch {
emitDeviation(it.second)
}
}
}
deviationFlow.buffer(COROUTINE_BUFFER_SIZE)
.fold(null) { prev: Candidate?, other: Candidate ->
when {
prev === null -> other
prev.third > other.third -> other
else -> prev
}
}
?.also { this@bestCenters.send(center to it) }
}
}
}
}.toList().sortedBy { (_, candidate) -> candidate.third }
// filter all candidates that are too close to each other
return bestCenters
.filterIndexed { index, (center, _) ->
(0 until index).map { bestCenters[it] }
.none { (betterCenter, _) ->
(betterCenter - center).vec2D().magnitude < golden.orientationRadius
}
}.map { golden to it }
}
private suspend fun findGoldenPoIAnimate(
golden: Golden,
distanceMultiplier: Scalar,
identity: IdentifiableEntity,
centrumMask: Mat,
image: Mat,
displayImage: Mat,
windowMgr: IWindowManager,
parentScope: CoroutineScope,
): List<Pair<Golden, Pair<Pixel, Candidate>>> {
withTmpMat { tmp ->
identity.distanceMat.convertTo(tmp, CvType.CV_8U)
Core.multiply(tmp, distanceMultiplier, tmp)
displayImage.withSubmatSafe(identity.rect) { target ->
Imgproc.cvtColor(tmp, tmp, Imgproc.COLOR_GRAY2BGR)
tmp.copyTo(target, identity.entityMask)
tmp.setTo(COLOR_BLACK)
tmp.copyTo(target, centrumMask)
Imgproc.cvtColor(centrumMask, tmp, Imgproc.COLOR_GRAY2BGR)
Core.multiply(tmp, golden.goldenColor, tmp, 1.0)
Core.add(tmp, target, target, centrumMask, CvType.CV_8UC3)
}
}
windowMgr.updateImage(displayImage)
delay(ANIMATION_DELAY)
return findGoldenPoI(
image = image,
identity = identity,
golden = golden,
centrumMask = centrumMask
).also { goldenCandidates ->
val goldenDirection = Vector2D(golden.orientation.vec2D()).normalize()
withContext(SINGLE_DISPATCHER) {
withTmpMat { tmp ->
withTmpMat { displayTmp ->
goldenCandidates.forEach { (golden, centerToCandidate) ->
val (center, candidate) = centerToCandidate
val intoGoldSpace = golden.minMaxDistance.maxLoc.rounded() - center
val rect = golden.rect.apply {
x += identity.rect.x - intoGoldSpace.x
y += identity.rect.y - intoGoldSpace.y
}
image.withSubmatSafe(rect) { subImage ->
subImage.copyTo(displayTmp)
val (endpix, dirVec, deviation) = candidate
val wmg = WINDOW_MANAGER_FACTORY.createWindowManager("$golden on ${identity.rect} with $deviation")
wmg.setDimension(HIST_SIZE, HIST_SIZE)
parentScope.launch {
wmg.display(displayTmp)
}
val angle = acos(dirVec.dot(goldenDirection))
subImage.copyTo(tmp)
Imgproc.cvtColor(tmp, displayTmp, Imgproc.COLOR_GRAY2BGR)
displayTmp.drawLine(
golden.minMaxDistance.maxLoc.rounded() to intoGoldSpace + endpix,
COLOR_GREEN,
)
launch {
wmg.updateImage(displayTmp)
}
delay(ANIMATION_DELAY * 1000)
CvContext.withRotationMatrix(
angle = angle,
center = golden.minMaxDistance.maxLoc,
) { intoGoldSpaceRot ->
Imgproc.warpAffine(tmp, tmp, intoGoldSpaceRot, tmp.size())
}
Imgproc.cvtColor(tmp, displayTmp, Imgproc.COLOR_GRAY2BGR)
displayTmp.drawLine(
golden.orientation,
COLOR_YELLOW,
)
launch {
wmg.updateImage(displayTmp)
}
delay(ANIMATION_DELAY * 1000)
Core.absdiff(golden.image, tmp, tmp)
Imgproc.filter2D(tmp, displayTmp, -1, CvContext.gaussianKernel)
launch {
wmg.updateImage(displayTmp)
}
delay(ANIMATION_DELAY)
wmg.updateImage(displayTmp)
delay(10.seconds)
wmg.dispose()
}
}
}
}
}
}
}
suspend fun main(args: Array<String>) {
CvContext.initialize()
val imageDirectory = Path(args.getOrElse(0) { "." })
val comparisonData = imageDirectory.resolve("export_data.csv").bufferedReader().lineSequence().drop(1)
.map { line -> line.trimEnd().split(",") }
.groupingBy { parts -> parts[0] }
.aggregateTo(ConcurrentHashMap()) { _, accumulator: MutableList<Pair<String, Rect>>?, parts, _ ->
val descriptionToRect = parts[5] to Rect(
parts[1].toInt(),
parts[2].toInt(),
parts[3].toInt() - parts[1].toInt() + 1,
parts[4].toInt() - parts[2].toInt() + 1,
)
(accumulator ?: mutableListOf()).apply { add(descriptionToRect) }
}
val hits = AtomicInteger(0)
val falseHit = AtomicInteger(0)
val count = AtomicInteger(0)
val imagePaths = imageDirectory.listDirectoryEntries("*.bmp")
val perImageWindow = WINDOW_MANAGER_FACTORY.createWindowManager("Current Image")
val scanner = EntityScanner(animate = false, scanBarSpacing = 6, foregroundThreshold = FOREGROUND_THRESHOLD)
coroutineScope {
val chaetomium = Golden.createGolden(
parentScope = this,
scanner = scanner,
goldenName = "Chaetomium",
goldenColor = COLOR_GREEN,
goldenPath = Resource.getPath("single_chaetomium.bmp"),
deviationParameters = DeviationParameters(
deviationMin = 40,
deviationFunc = { level, value ->
level * (value / 40f)
},
maximumDeviation = 100.0
),
)
val stachybotry = Golden.createGolden(
parentScope = this,
scanner = scanner,
goldenName = "Stachybotrys",
goldenPath = Resource.getPath("single_stachybotry.bmp"),
goldenColor = COLOR_RED,
deviationParameters = DeviationParameters(
deviationMin = 20,
deviationFunc = { level, value ->
level * (value / 12f)
},
maximumDeviation = 200.0
),
)
val goldenList = listOf(chaetomium, stachybotry)
val localBufferInfo = when (COROUTINE_BUFFER_SIZE) {
Channel.RENDEZVOUS -> "RENDEZVOUS"
Channel.CONFLATED -> "CONFLATED"
Channel.BUFFERED -> "BUFFERED"
Channel.UNLIMITED -> "UNLIMITED"
else -> toString()
}
println("Coroutine buffer: $localBufferInfo, Parallel images: $MAXIMUM_PARALLEL_IMAGES")
val deferred = async {
val maxDistance = goldenList.maxOf { it.minMaxDistance.maxVal }
val distanceMultiplier = COLOR_WHITE / maxDistance
if (ANIMATION_DELAY.isPositive()) {
goldenList.forEach {
Core.multiply(it.displayImage, distanceMultiplier, it.displayImage)
launch {
it.updateImage()
}
if (ANIMATION_DELAY.isPositive()) delay(ANIMATION_DELAY)
Imgproc.cvtColor(it.image, it.displayImage, Imgproc.COLOR_GRAY2BGR)
it.displayImage.drawLine(it.orientation, COLOR_YELLOW)
it.updateImage()
if (ANIMATION_DELAY.isPositive()) delay(ANIMATION_DELAY)
}
}
val windowInitialized = AtomicBoolean(false)
val imageFlow = flow {
for (imagePath in imagePaths.shuffled()) {
try {
val (displayImage, image) = CvContext.readAndConvertImage(imagePath)
if (!windowInitialized.getAndSet(true) && ANIMATION_DELAY.isPositive()) {
this@coroutineScope.launch {
try {
perImageWindow.display(displayImage)
} catch (e: CancellationException) {
this@async.cancel("Window was closed", e)
}
}
}
val comparisonSet: MutableList<Pair<String, Rect>> = comparisonData[imagePath.name] ?: mutableListOf()
emit(Triple(displayImage, image, comparisonSet))
} catch (error: CvException) {
System.err.println("Cannot read $imagePath: ${error.message}")
}
}
}
imageFlow
.buffer(if (ANIMATION_DELAY.isPositive()) Channel.RENDEZVOUS else MAXIMUM_PARALLEL_IMAGES)
.collect { (displayImage, image, comparison) ->
scanner.scanImage(image, displayImage, perImageWindow).asFlow()
.buffer(COROUTINE_BUFFER_SIZE)
.mapNotNull entityMatching@{ entity ->
withIdEntity(entity) { identity ->
var goldenCandidates = goldenList.flatMap { golden ->
golden.withCentrumMask(identity) { centrumMask ->
if (ANIMATION_DELAY.isPositive()) {
findGoldenPoIAnimate(
golden = golden,
distanceMultiplier = distanceMultiplier,
identity = identity,
centrumMask = centrumMask,
image = image,
displayImage = displayImage,
windowMgr = perImageWindow,
parentScope = this@coroutineScope,
)
} else {
findGoldenPoI(
image = image,
identity = identity,
golden = golden,
centrumMask = centrumMask,
)
}
}
}.sortedBy { it.second.second.third } // deviation
goldenCandidates = goldenCandidates
.filterIndexed { index, (_, it) ->
val (center, _) = it
(0 until index).map { goldenCandidates[it] }
.none { (betterGold, other) ->
(other.first - center).vec2D().magnitude < betterGold.orientationRadius
}
}
if (ANIMATION_DELAY.isPositive()) {
goldenCandidates = goldenCandidates.onEach { (gold, it) ->
val (center, candidate) = it
val (_, _, deviation) = candidate
println("Found minimum for ${identity.rect} on $gold: $center (deviation: $deviation)")
Imgproc.drawMarker(
/* img = */ displayImage,
/* position = */ center.vec2D() + identity.rect.tl(),
/* color = */ gold.goldenColor,
/* markerType = */ Imgproc.MARKER_CROSS,
/* markerSize = */ 3,
/* thickness = */ 3,
)
Imgproc.rectangle(displayImage, identity.rect, gold.goldenColor)
perImageWindow.updateImage(displayImage)
if (ANIMATION_DELAY.isPositive()) delay(ANIMATION_DELAY)
}
}
return@entityMatching goldenCandidates.groupingBy { (gold, _) -> gold.goldenName }
.eachCount()
.maxByOrNull { it.value }
?.let { goldenAmount ->
val description = if (goldenAmount.value > 1) {
"${goldenAmount.key}_Agglomerate_Candidate"
} else {
"${goldenAmount.key}_Einzelspore_Candidate"
}
Triple(description, identity.rect, comparison)
}
}
}
.collect { result ->
count.incrementAndGet()
val matchingResults = result.third.filter { (description, comparisonRect) ->
description == result.first &&
(comparisonRect.contains(result.second.tl()) &&
comparisonRect.contains(result.second.br())) ||
(result.second.contains(comparisonRect.tl()) &&
result.second.contains(comparisonRect.br()))
}
result.third.removeAll(matchingResults)
when (matchingResults.size) {
0 -> falseHit.incrementAndGet().also { println("False hit ${result.first}") }
1 -> hits.incrementAndGet().also { println("Hit ${result.first}") }
else -> falseHit.incrementAndGet().also { println("Hit too many ${result.first}") }
}
}
if (ANIMATION_DELAY.isPositive()) delay(ANIMATION_DELAY * 2000)
println("Releasing image $displayImage\n and $image")
displayImage.release()
CvContext.recycle(image)
}
}
deferred.invokeOnCompletion {
perImageWindow.dispose()
goldenList.forEach { it.release() }
if (deferred.isCancelled) {
val error = deferred.getCompletionExceptionOrNull()!!
if (error is CancellationException) {
val cause = error.cause?.takeIf { it.message != error.message }?.let { ". Cause: ".plus(it.message) }.orEmpty()
System.err.println("Cancelled program. Cause: ${error.message}" + cause)
} else {
error.printStackTrace()
}
exitProcess(1)
}
}
deferred.join()
val misses = comparisonData.values.sumOf { it.size }
println("Hits ${hits.get()}, False-hits ${falseHit.get()}, Total ${count.get()}, Misses $misses")
}
}
internal inline fun <T> Mat.withSubmatSafe(rect: Rect, block: (mat: Mat) -> T): T? {
val submat = try {
submat(rect)
} catch (error: CvException) {
System.err.println("Cannot create submatrix for $this, rect $rect, error:\n $error")
null
}
return submat?.let { mat ->
kotlin.runCatching { block(mat) }.also { mat.release() }.getOrThrow()
}
}
private fun ClosedFloatingPointRange<Double>.progression(step: Number): List<Double> = sequence {
var x = start
while (lessThanOrEquals(x, endInclusive)) {
yield(x)
x += step.toDouble()
}
}.toList()

View file

@ -0,0 +1,41 @@
package network.rs485.ben.computervision
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
internal class LineTest {
@Test
fun `line intersect fails for lines on different y`() {
val a = Pixel(0, 0) to Pixel(1, 0)
val b = Pixel(0, 1) to Pixel(1, 1)
assertFalse { a.intersects(b) }
assertFalse { b.intersects(a) }
}
@Test
fun `line intersect fails for unconnected lines`() {
val a = Pixel(0, 0) to Pixel(1, 0)
val b = Pixel(2, 0) to Pixel(3, 0)
assertFalse { a.intersects(b) }
assertFalse { b.intersects(a) }
}
@Test
fun `line intersect succeeds for connected lines`() {
val a = Pixel(0, 0) to Pixel(2, 0)
val b = Pixel(1, 0) to Pixel(3, 0)
assertTrue { a.intersects(b) }
assertTrue { b.intersects(a) }
}
@Test
fun `line intersect succeeds for stacked lines`() {
val a = Pixel(0, 0) to Pixel(3, 0)
val b = Pixel(1, 0) to Pixel(2, 0)
assertTrue { a.intersects(b) }
assertTrue { b.intersects(a) }
}
}