Browse Source

More refactoring

master
Drew Short 10 years ago
parent
commit
783a3a4aba
  1. 34
      cli/pom.xml
  2. 18
      cli/src/includes/logback.xml
  3. 6
      cli/src/main/java/com/sothr/imagetools/cli/AppCLI.java
  4. 2
      cli/version.info
  5. 41
      engine/pom.xml
  6. 62
      engine/src/main/java/com/sothr/imagetools/engine/AppConfig.java
  7. 21
      engine/src/main/java/com/sothr/imagetools/engine/errors/ImageToolsException.java
  8. 34
      engine/src/main/java/com/sothr/imagetools/engine/util/ResourceLoader.java
  9. 2
      engine/src/main/resources/application.conf
  10. 2
      engine/src/main/resources/ehcache.xml
  11. 9
      engine/src/main/resources/hibernate.cfg.xml
  12. 3
      engine/src/main/resources/hibernate/Image.hbm.xml
  13. 4
      engine/src/main/resources/logback-minimum-config.xml
  14. 322
      engine/src/main/scala/com/sothr/imagetools/engine/ConcurrentEngine.scala
  15. 46
      engine/src/main/scala/com/sothr/imagetools/engine/Engine.scala
  16. 19
      engine/src/main/scala/com/sothr/imagetools/engine/SequentialEngine.scala
  17. 16
      engine/src/main/scala/com/sothr/imagetools/engine/dao/HibernateUtil.scala
  18. 14
      engine/src/main/scala/com/sothr/imagetools/engine/dao/ImageDAO.scala
  19. 53
      engine/src/main/scala/com/sothr/imagetools/engine/dto/ImageHashDTO.scala
  20. 78
      engine/src/main/scala/com/sothr/imagetools/engine/hash/AHash.scala
  21. 38
      engine/src/main/scala/com/sothr/imagetools/engine/hash/DHash.scala
  22. 80
      engine/src/main/scala/com/sothr/imagetools/engine/hash/HashService.scala
  23. 112
      engine/src/main/scala/com/sothr/imagetools/engine/hash/PHash.scala
  24. 2
      engine/src/main/scala/com/sothr/imagetools/engine/hash/PerceptualHasher.scala
  25. 98
      engine/src/main/scala/com/sothr/imagetools/engine/image/Image.scala
  26. 4
      engine/src/main/scala/com/sothr/imagetools/engine/image/ImageFilter.scala
  27. 67
      engine/src/main/scala/com/sothr/imagetools/engine/image/ImageService.scala
  28. 22
      engine/src/main/scala/com/sothr/imagetools/engine/image/SimilarImages.scala
  29. 49
      engine/src/main/scala/com/sothr/imagetools/engine/util/PropertiesService.scala
  30. 20
      engine/src/main/scala/com/sothr/imagetools/engine/util/Timing.scala
  31. 46
      engine/src/main/scala/com/sothr/imagetools/engine/util/Version.scala
  32. 46
      engine/src/test/java/com/sothr/imagetools/engine/AppTest.java
  33. 2
      engine/src/test/resources/application.conf
  34. 9
      engine/src/test/resources/hibernate.cfg.xml
  35. 3
      engine/src/test/resources/hibernate/Image.hbm.xml
  36. 12
      engine/src/test/resources/logback-minimum-config.xml
  37. 32
      engine/src/test/scala/com/sothr/imagetools/engine/EngineTest.scala
  38. 6
      engine/src/test/scala/com/sothr/imagetools/engine/TestParams.scala
  39. 145
      engine/src/test/scala/com/sothr/imagetools/engine/hash/HashServiceTest.scala
  40. 22
      engine/src/test/scala/com/sothr/imagetools/engine/image/ImageFilterTest.scala
  41. 34
      gui/pom.xml
  42. 18
      gui/src/includes/logback.xml
  43. 114
      gui/src/main/java/com/sothr/imagetools/ui/App.java
  44. 170
      gui/src/main/resources/fxml/mainapp/MainApp.fxml
  45. 6
      gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTile.scala
  46. 27
      gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTileFactory.scala
  47. 74
      gui/src/main/scala/com/sothr/imagetools/ui/controller/AppController.scala
  48. 2
      gui/version.info
  49. 46
      parent/pom.xml
  50. 2
      pom.xml

34
cli/pom.xml

@ -1,5 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@ -27,16 +27,16 @@
<artifactId>ImageTools-Engine</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
@ -80,18 +80,18 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>prepare</id>
<phase>process-resources</phase>
<configuration>
<phase>process-resources</phase>
<configuration>
<tasks>
<copy file="${project.build.directory}/version.info" toFile="${basedir}/version.info" overwrite="true" />
<copy file="${project.build.directory}/name.info" toFile="${basedir}/name.info" overwrite="true" />
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE" overwrite="true" />
<copy file="${project.build.directory}/version.info" toFile="${basedir}/version.info" overwrite="true"/>
<copy file="${project.build.directory}/name.info" toFile="${basedir}/name.info" overwrite="true"/>
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE" overwrite="true"/>
<chmod file="${project.build.directory}/startCLI.sh" perm="755"/>
</tasks>
</configuration>
@ -112,7 +112,7 @@
<goal>run</goal>
</goals>
</execution>
</executions>
</executions>
</plugin>
</plugins>
</build>

18
cli/src/includes/logback.xml

@ -4,10 +4,10 @@
<logger name="net.sf.ehcache" level="WARN"/>
<appender name="C" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Sorry Windows Users -->
<withJansi>false</withJansi>
<!-- Sorry Windows Users -->
<withJansi>false</withJansi>
<pattern>[%date{HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
@ -16,9 +16,9 @@
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.debug</File>
<encoder>
<withJansi>false</withJansi>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
@ -34,9 +34,9 @@
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.info</File>
<encoder>
<withJansi>false</withJansi>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
@ -52,9 +52,9 @@
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<withJansi>false</withJansi>
<pattern>[%.16thread] [%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>

6
cli/src/main/java/com/sothr/imagetools/cli/AppCLI.java

@ -39,8 +39,8 @@ class AppCLI {
AppConfig.shutdown();
}
System.exit(0);
} catch (Exception ex) {
logger.error("Unhandled exception in AppCLI",ex);
} catch (Exception ex) {
logger.error("Unhandled exception in AppCLI", ex);
}
}
@ -97,7 +97,7 @@ class AppCLI {
Boolean recursive = false;
Integer recursiveDepth = 500;
if (cmd.hasOption('r')) {
recursive = true;
recursive = true;
}
if (cmd.hasOption('d')) {
recursiveDepth = Integer.parseInt(cmd.getOptionValue('d'));

2
cli/version.info

@ -1 +1 @@
0.1.1-DEV-13-661ba6d
0.1.1-DEV-15-15ce4f1

41
engine/pom.xml

@ -1,5 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@ -33,16 +33,16 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
@ -105,8 +105,8 @@
<artifactId>hibernate-ehcache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
@ -180,29 +180,30 @@
</plugin>
<!-- General Ant Tasks
Mostly Moving Files -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>prepare</id>
<phase>process-resources</phase>
<configuration>
<phase>process-resources</phase>
<configuration>
<tasks>
<!-- Copy hibernate configuration files -->
<copy todir="${basedir}/src/test/resources/hibernate">
<fileset dir="${basedir}/src/main/resources/hibernate" includes="**/*" />
<fileset dir="${basedir}/src/main/resources/hibernate" includes="**/*"/>
</copy>
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE" overwrite="true" />
<copy file="${project.build.directory}/version.info" toFile="${basedir}/../version.info" overwrite="true" />
<copy file="${project.build.directory}/README.md" toFile="${basedir}/../README.md" overwrite="true" />
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE" overwrite="true"/>
<copy file="${project.build.directory}/version.info" toFile="${basedir}/../version.info"
overwrite="true"/>
<copy file="${project.build.directory}/README.md" toFile="${basedir}/../README.md" overwrite="true"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</executions>
</plugin>
</plugins>
</build>

62
engine/src/main/java/com/sothr/imagetools/engine/AppConfig.java

@ -37,53 +37,59 @@ public class AppConfig {
// The Main App
private static Stage primaryStage = null;
public static Stage getPrimaryStage() { return primaryStage; }
public static void setPrimaryStage(Stage newPrimaryStage) { primaryStage = newPrimaryStage; }
public static Stage getPrimaryStage() {
return primaryStage;
}
public static void setPrimaryStage(Stage newPrimaryStage) {
primaryStage = newPrimaryStage;
}
public static ActorSystem getAppActorSystem() {
return appSystem;
}
public static void configureApp() {
logger = (Logger)LoggerFactory.getLogger(AppConfig.class);
logger = (Logger) LoggerFactory.getLogger(AppConfig.class);
loadProperties();
configLogging();
configCache();
}
private static void configLogging(String location) {
private static void configLogging(String location) {
//Logging Config
//remove previous configuration if it exists
Logger rootLogger = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
LoggerContext context = rootLogger.getLoggerContext();
context.reset();
File file = new File(location);
Boolean fromFile = false;
if (file.exists()) {
fromFile = true;
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(location);
} catch (JoranException je) {
// StatusPrinter will handle this
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
fromFile = true;
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(location);
} catch (JoranException je) {
// StatusPrinter will handle this
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
} else {
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(ResourceLoader.get().getResourceStream("logback-minimum-config.xml"));
} catch (JoranException je) {
// StatusPrinter will handle this
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(ResourceLoader.get().getResourceStream("logback-minimum-config.xml"));
} catch (JoranException je) {
// StatusPrinter will handle this
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
}
String message = fromFile ? "From File" : "From Defaults";
logger.info(String.format("Configured Logger %s", message));

21
engine/src/main/java/com/sothr/imagetools/engine/errors/ImageToolsException.java

@ -2,14 +2,25 @@ package com.sothr.imagetools.engine.errors;
/**
* Simple Exception
*
* <p/>
* Created by drew on 12/31/13.
*/
public class ImageToolsException extends Exception {
public ImageToolsException() { super(); }
public ImageToolsException(String message) { super(message); }
public ImageToolsException(String message, Throwable cause) { super(message, cause); }
public ImageToolsException(Throwable cause) { super(cause); }
public ImageToolsException() {
super();
}
public ImageToolsException(String message) {
super(message);
}
public ImageToolsException(String message, Throwable cause) {
super(message, cause);
}
public ImageToolsException(Throwable cause) {
super(cause);
}
}

34
engine/src/main/java/com/sothr/imagetools/engine/util/ResourceLoader.java

@ -8,31 +8,31 @@ import java.net.URL;
/**
* Seamlessly handle resource loading
*
* <p/>
* Created by drew on 1/5/14.
*/
public class ResourceLoader {
private static final ResourceLoader instance = new ResourceLoader();
private static final ResourceLoader instance = new ResourceLoader();
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private ResourceLoader() {
logger.info("Created Resource Loader");
}
private ResourceLoader() {
logger.info("Created Resource Loader");
}
public static ResourceLoader get() {
return instance;
}
public static ResourceLoader get() {
return instance;
}
public URL getResource(String location) {
logger.debug(String.format("Attempting to load resource: %s", location));
return Thread.currentThread().getContextClassLoader().getResource(location);
}
public URL getResource(String location) {
logger.debug(String.format("Attempting to load resource: %s", location));
return Thread.currentThread().getContextClassLoader().getResource(location);
}
public InputStream getResourceStream(String location) {
logger.debug(String.format("Attempting to get stream for resource: %s",location));
return Thread.currentThread().getContextClassLoader().getResourceAsStream(location);
}
public InputStream getResourceStream(String location) {
logger.debug(String.format("Attempting to get stream for resource: %s", location));
return Thread.currentThread().getContextClassLoader().getResourceAsStream(location);
}
}

2
engine/src/main/resources/application.conf

@ -23,7 +23,7 @@ app {
differenceThreshold = 0.90
//control generation of hashes for new images.
hash {
precision=64
precision = 64
}
ahash {
use = true

2
engine/src/main/resources/ehcache.xml

@ -1,4 +1,4 @@
<ehcache maxBytesLocalHeap="512M" maxBytesLocalDisk="5g" >
<ehcache maxBytesLocalHeap="512M" maxBytesLocalDisk="5g">
<diskStore path=".cache/ehcache"/>
<cache name="images"
timeToLiveSeconds="100">

9
engine/src/main/resources/hibernate.cfg.xml

@ -25,15 +25,18 @@
<!-- Enable the second-level cache -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
</property>
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="c3p0.acquire_increment">1</property>
<property name="c3p0.idle_test_period">100</property> <!-- seconds -->
<property name="c3p0.idle_test_period">100</property>
<!-- seconds -->
<property name="c3p0.max_size">50</property>
<property name="c3p0.max_statements">0</property>
<property name="c3p0.min_size">5</property>
<property name="c3p0.timeout">100</property> <!-- seconds -->
<property name="c3p0.timeout">100</property>
<!-- seconds -->
<!-- mapping files -->
<mapping resource="hibernate/Image.hbm.xml"/>

3
engine/src/main/resources/hibernate/Image.hbm.xml

@ -11,6 +11,7 @@
<property name="thumbnailPath" column="thumbnail_path" type="string" not-null="true"/>
<property name="width" column="width" type="int" not-null="true"/>
<property name="height" column="height" type="int" not-null="true"/>
<many-to-one name="hashes" column="hashes" unique="true" class="com.sothr.imagetools.engine.dto.ImageHashDTO" cascade="save-update, delete" not-null="true" lazy="false"/>
<many-to-one name="hashes" column="hashes" unique="true" class="com.sothr.imagetools.engine.dto.ImageHashDTO"
cascade="save-update, delete" not-null="true" lazy="false"/>
</class>
</hibernate-mapping>

4
engine/src/main/resources/logback-minimum-config.xml

@ -3,9 +3,9 @@
<appender name="EL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<withJansi>false</withJansi>
<pattern>[%date{HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>

322
engine/src/main/scala/com/sothr/imagetools/engine/ConcurrentEngine.scala

@ -8,8 +8,7 @@ import akka.pattern.ask
import akka.routing.{Broadcast, RoundRobinRouter, SmallestMailboxRouter}
import akka.util.Timeout
import com.sothr.imagetools.engine.hash.HashService
import com.sothr.imagetools.engine.image.{SimilarImages, ImageService, Image}
import com.sothr.imagetools.image.SimilarImages
import com.sothr.imagetools.engine.image.{Image, ImageService, SimilarImages}
import com.sothr.imagetools.engine.util._
import scala.collection.mutable
@ -28,10 +27,10 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
engineSimilarityController ! SetNewListener(listenerRef)
}
def getImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[Image] = {
def getImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[Image] = {
debug(s"Looking for images in directory: $directoryPath")
val imageFiles = getAllImageFiles(directoryPath, recursive, recursiveDepth)
val images:mutable.MutableList[Image] = new mutable.MutableList[Image]()
val images: mutable.MutableList[Image] = new mutable.MutableList[Image]()
// make sure the engine is listening
engineProcessingController ! EngineStart
for (file <- imageFiles) {
@ -39,18 +38,18 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
}
engineProcessingController ! EngineNoMoreFiles
var doneProcessing = false
while(!doneProcessing) {
while (!doneProcessing) {
val f = engineProcessingController ? EngineIsProcessingFinished
val result = Await.result(f, timeout.duration).asInstanceOf[Boolean]
result match {
case true =>
doneProcessing = true
debug("Processing Complete")
case false =>
case false =>
debug("Still Processing")
//sleep thread
Thread.sleep(5000L)
//val future = Future { blocking(Thread.sleep(5000L)); "done" }
//val future = Future { blocking(Thread.sleep(5000L)); "done" }
}
}
val f = engineProcessingController ? EngineGetProcessingResults
@ -60,7 +59,7 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
}
//needs to be rebuilt
def getSimilarImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[SimilarImages] = {
def getSimilarImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[SimilarImages] = {
debug(s"Looking for similar images in directory: $directoryPath")
val images = getImagesForDirectory(directoryPath, recursive, recursiveDepth)
info(s"Searching ${images.length} images for similarities")
@ -73,7 +72,7 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
//tell the comparison engine there's nothing left to compare
engineSimilarityController ! EngineNoMoreComparisons
var doneProcessing = false
while(!doneProcessing) {
while (!doneProcessing) {
val f = engineSimilarityController ? EngineIsSimilarityFinished
val result = Await.result(f, timeout.duration).asInstanceOf[Boolean]
result match {
@ -94,17 +93,17 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
val cleanedSimilarImages = new mutable.MutableList[SimilarImages]()
val ignoreSet = new mutable.HashSet[Image]()
for (similarImages <- result) {
count += 1
if (count % 25 == 0 || count == result.length) debug(s"Cleaning similar image $count/${result.length} ${result.length - count} left to clean")
if (!ignoreSet.contains(similarImages.rootImage)) {
cleanedSimilarImages += similarImages
ignoreSet += similarImages.rootImage
for (image <- similarImages.similarImages) {
ignoreSet += image
}
count += 1
if (count % 25 == 0 || count == result.length) debug(s"Cleaning similar image $count/${result.length} ${result.length - count} left to clean")
if (!ignoreSet.contains(similarImages.rootImage)) {
cleanedSimilarImages += similarImages
ignoreSet += similarImages.rootImage
for (image <- similarImages.similarImages) {
ignoreSet += image
}
}
}
var similarCount = 0
for (similarImage <- cleanedSimilarImages) {
similarCount += 1 + similarImage.similarImages.size
@ -118,164 +117,176 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
// external cases //
case class SetNewListener(listenerType: ActorRef)
case object EngineStart
// processing files into images
case class EngineProcessFile(file:File)
case class EngineProcessFile(file: File)
case object EngineNoMoreFiles
case object EngineIsProcessingFinished
case object EngineGetProcessingResults
//internal cases
case class EngineFileProcessed(image:Image)
case class EngineFileProcessed(image: Image)
case object EngineActorProcessingFinished
case object EngineActorReactivate
class ConcurrentEngineProcessingController extends Actor with ActorLogging {
val numOfRouters = {
val max = PropertiesService.get(PropertyEnum.ConcurrentProcessingLimit.toString).toInt
val processors = Runtime.getRuntime.availableProcessors()
var threads = 0
if (processors > max) threads = max else if (processors > 1) threads = processors - 1 else threads = 1
threads
}
var router = context.actorOf(Props[ConcurrentEngineProcessingActor].withRouter(SmallestMailboxRouter(nrOfInstances = numOfRouters)))
var images:mutable.MutableList[Image] = new mutable.MutableList[Image]()
var toProcess = 0
var processed = 0
var processorsFinished = 0
var listener = context.actorOf(Props[DefaultLoggingEngineListener],
name = "ProcessedEngineListener")
def setListener(newListener: ActorRef) = {
//remove the old listener
this.listener ! PoisonPill
//setup the new listener
this.listener = newListener
}
val numOfRouters = {
val max = PropertiesService.get(PropertyEnum.ConcurrentProcessingLimit.toString).toInt
val processors = Runtime.getRuntime.availableProcessors()
var threads = 0
if (processors > max) threads = max else if (processors > 1) threads = processors - 1 else threads = 1
threads
}
var router = context.actorOf(Props[ConcurrentEngineProcessingActor].withRouter(SmallestMailboxRouter(nrOfInstances = numOfRouters)))
override def preStart() = {
log.info("Staring the controller for processing images")
log.info("Using {} actors to process images", numOfRouters)
}
override def receive = {
case command:SetNewListener => setListener(command.listenerType)
case command:EngineProcessFile => processFile(command)
case command:EngineFileProcessed => fileProcessed(command)
case EngineStart => startEngine()
case EngineNoMoreFiles => requestWrapup()
case EngineActorProcessingFinished => actorProcessingFinished()
case EngineIsProcessingFinished => checkIfProcessingIsFinished()
case EngineGetProcessingResults => checkForResults()
case _ => log.info("received unknown message")
}
var images: mutable.MutableList[Image] = new mutable.MutableList[Image]()
var toProcess = 0
var processed = 0
override def postStop() = {
super.postStop()
this.listener ! PoisonPill
}
var processorsFinished = 0
var listener = context.actorOf(Props[DefaultLoggingEngineListener],
name = "ProcessedEngineListener")
def startEngine() = {
router ! Broadcast(EngineActorReactivate)
}
def setListener(newListener: ActorRef) = {
//remove the old listener
this.listener ! PoisonPill
//setup the new listener
this.listener = newListener
}
def processFile(command:EngineProcessFile) = {
log.debug(s"Started evaluating ${command.file.getAbsolutePath}")
toProcess += 1
router ! command
}
def fileProcessed(command:EngineFileProcessed) = {
processed += 1
if (processed % 25 == 0 || processed == toProcess) {
//log.info(s"Processed $processed/$toProcess")
listener ! ComparedFileCount(processed,toProcess)
}
if (command.image != null) {
log.debug(s"processed image: ${command.image.imagePath}")
images += command.image
}
}
def requestWrapup() = {
router ! Broadcast(EngineNoMoreFiles)
override def preStart() = {
log.info("Staring the controller for processing images")
log.info("Using {} actors to process images", numOfRouters)
}
override def receive = {
case command: SetNewListener => setListener(command.listenerType)
case command: EngineProcessFile => processFile(command)
case command: EngineFileProcessed => fileProcessed(command)
case EngineStart => startEngine()
case EngineNoMoreFiles => requestWrapup()
case EngineActorProcessingFinished => actorProcessingFinished()
case EngineIsProcessingFinished => checkIfProcessingIsFinished()
case EngineGetProcessingResults => checkForResults()
case _ => log.info("received unknown message")
}
override def postStop() = {
super.postStop()
this.listener ! PoisonPill
}
def startEngine() = {
router ! Broadcast(EngineActorReactivate)
}
def processFile(command: EngineProcessFile) = {
log.debug(s"Started evaluating ${command.file.getAbsolutePath}")
toProcess += 1
router ! command
}
def fileProcessed(command: EngineFileProcessed) = {
processed += 1
if (processed % 25 == 0 || processed == toProcess) {
//log.info(s"Processed $processed/$toProcess")
listener ! ComparedFileCount(processed, toProcess)
}
/*
* Record that a processor is done
*/
def actorProcessingFinished() = {
processorsFinished += 1
if (command.image != null) {
log.debug(s"processed image: ${command.image.imagePath}")
images += command.image
}
/*
* Check if processing is done
*/
def checkIfProcessingIsFinished() = {
try {
if (processorsFinished >= numOfRouters) sender ! true else sender ! false
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
def requestWrapup() = {
router ! Broadcast(EngineNoMoreFiles)
}
/*
* Record that a processor is done
*/
def actorProcessingFinished() = {
processorsFinished += 1
}
/*
* Check if processing is done
*/
def checkIfProcessingIsFinished() = {
try {
if (processorsFinished >= numOfRouters) sender ! true else sender ! false
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
/*
* Get the results of the processing
*/
def checkForResults() = {
try {
processorsFinished = 0
toProcess = 0
processed = 0
sender ! images.toList
images.clear()
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
/*
* Get the results of the processing
*/
def checkForResults() = {
try {
processorsFinished = 0
toProcess = 0
processed = 0
sender ! images.toList
images.clear()
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
}
class ConcurrentEngineProcessingActor extends Actor with ActorLogging {
var ignoreMessages = false
override def receive = {
case command:EngineProcessFile => processFile(command)
case EngineNoMoreFiles => finishedProcessingFiles()
case EngineActorReactivate => ignoreMessages = false
case _ => log.info("received unknown message")
}
def processFile(command:EngineProcessFile) = {
if (!ignoreMessages) {
val image = ImageService.getImage(command.file)
if (image != null) {
sender ! EngineFileProcessed(image)
} else {
log.debug(s"Failed to process image: ${command.file.getAbsolutePath}")
}
}
var ignoreMessages = false
override def receive = {
case command: EngineProcessFile => processFile(command)
case EngineNoMoreFiles => finishedProcessingFiles()
case EngineActorReactivate => ignoreMessages = false
case _ => log.info("received unknown message")
}
def processFile(command: EngineProcessFile) = {
if (!ignoreMessages) {
val image = ImageService.getImage(command.file)
if (image != null) {
sender ! EngineFileProcessed(image)
} else {
log.debug(s"Failed to process image: ${command.file.getAbsolutePath}")
}
}
def finishedProcessingFiles() = {
if (!ignoreMessages) {
ignoreMessages = true
sender ! EngineActorProcessingFinished
}
}
def finishedProcessingFiles() = {
if (!ignoreMessages) {
ignoreMessages = true
sender ! EngineActorProcessingFinished
}
}
}
//finding similarities between images
case class EngineCompareImages(image1:Image, images:List[Image])
case class EngineCompareImagesComplete(similarImages:SimilarImages)
case class EngineCompareImages(image1: Image, images: List[Image])
case class EngineCompareImagesComplete(similarImages: SimilarImages)
case object EngineNoMoreComparisons
case object EngineIsSimilarityFinished
case object EngineGetSimilarityResults
case object EngineActorCompareImagesFinished
class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
@ -310,9 +321,9 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
}
override def receive = {
case command:SetNewListener => setListener(command.listenerType)
case command:EngineCompareImages => findSimilarities(command)
case command:EngineCompareImagesComplete => similarityProcessed(command)
case command: SetNewListener => setListener(command.listenerType)
case command: EngineCompareImages => findSimilarities(command)
case command: EngineCompareImagesComplete => similarityProcessed(command)
case EngineStart => startEngine()
case EngineNoMoreComparisons => requestWrapup()
case EngineActorCompareImagesFinished => actorProcessingFinished()
@ -330,22 +341,22 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
router ! Broadcast(EngineActorReactivate)
}
def findSimilarities(command:EngineCompareImages) = {
def findSimilarities(command: EngineCompareImages) = {
log.debug(s"Finding similarities between {} and {} images", command.image1.imagePath, command.images.length)
toProcess += 1
if (toProcess % 250 == 0) {
//log.info("Sent {} images to be processed for similarites", toProcess)
listener ! SubmitMessage(s"Sent $toProcess images to be processed for similarites")
//log.info("Sent {} images to be processed for similarites", toProcess)
listener ! SubmitMessage(s"Sent $toProcess images to be processed for similarites")
}
//just relay the command to our workers
router ! command
}
def similarityProcessed(command:EngineCompareImagesComplete) = {
def similarityProcessed(command: EngineCompareImagesComplete) = {
processed += 1
if (processed % 25 == 0 || processed == toProcess) {
//log.info(s"Processed $processed/$toProcess")
listener ! ScannedFileCount(processed,toProcess)
listener ! ScannedFileCount(processed, toProcess)
}
if (command.similarImages != null) {
log.debug(s"Found similar images: ${command.similarImages}")
@ -399,14 +410,15 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
class ConcurrentEngineSimilarityActor extends Actor with ActorLogging {
var ignoreMessages = false
override def receive = {
case command:EngineCompareImages => compareImages(command)
case command: EngineCompareImages => compareImages(command)
case EngineNoMoreComparisons => finishedComparisons()
case EngineActorReactivate => ignoreMessages = false
case _ => log.info("received unknown message")
}
def compareImages(command:EngineCompareImages) = {
def compareImages(command: EngineCompareImages) = {
if (!ignoreMessages) {
val similarImages = new mutable.MutableList[Image]()
for (image <- command.images) {

46
engine/src/main/scala/com/sothr/imagetools/engine/Engine.scala

@ -3,9 +3,7 @@ package com.sothr.imagetools.engine
import java.io.File
import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem}
import AppConfig
import com.sothr.imagetools.engine.image.{SimilarImages, ImageFilter, Image}
import com.sothr.imagetools.image.SimilarImages
import com.sothr.imagetools.engine.image.{Image, ImageFilter, SimilarImages}
import com.sothr.imagetools.engine.util.DirectoryFilter
import grizzled.slf4j.Logging
@ -18,16 +16,17 @@ import scala.collection.mutable
*/
abstract class Engine extends Logging {
val system = ActorSystem("EngineActorSystem")
val imageFilter:ImageFilter = new ImageFilter()
val imageFilter: ImageFilter = new ImageFilter()
val imageCache = AppConfig.cacheManager.getCache("images")
def setProcessedListener(listenerType: ActorRef)
def setSimilarityListener(listenerType: ActorRef)
def getAllImageFiles(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[File] = {
def getAllImageFiles(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[File] = {
val fileList = new mutable.MutableList[File]()
if (directoryPath != null && directoryPath != "") {
val directory:File = new File(directoryPath)
val directory: File = new File(directoryPath)
val imageFilter = new ImageFilter
if (directory.isDirectory) {
val files = directory.listFiles(imageFilter)
@ -38,7 +37,7 @@ abstract class Engine extends Logging {
val directoryFilter = new DirectoryFilter
val directories = directory.listFiles(directoryFilter)
for (directory <- directories) {
fileList ++= getAllImageFiles(directory.getAbsolutePath, recursive, recursiveDepth-1)
fileList ++= getAllImageFiles(directory.getAbsolutePath, recursive, recursiveDepth - 1)
}
}
}
@ -50,28 +49,33 @@ abstract class Engine extends Logging {
/**
* Get all images for a directory with hashes
*/
def getImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[Image]
def getImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[Image]
/**
* Get all similar images for a directory with hashes
*/
def getSimilarImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[SimilarImages]
def getSimilarImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[SimilarImages]
}
case class SubmitMessage(message:String)
case class ScannedFileCount(count:Integer, total:Integer, message:String=null)
case class ComparedFileCount(count:Integer,total:Integer, message:String=null)
case class SubmitMessage(message: String)
case class ScannedFileCount(count: Integer, total: Integer, message: String = null)
case class ComparedFileCount(count: Integer, total: Integer, message: String = null)
abstract class EngineListener extends Actor with ActorLogging {
override def receive: Actor.Receive = {
case command:SubmitMessage => handleMessage(command)
case command:ScannedFileCount => handleScannedFileCount(command)
case command:ComparedFileCount => handleComparedFileCount(command)
case command: SubmitMessage => handleMessage(command)
case command: ScannedFileCount => handleScannedFileCount(command)
case command: ComparedFileCount => handleComparedFileCount(command)
case _ => log.info("received unknown message")
}
def handleMessage(command:SubmitMessage)
def handleScannedFileCount(command:ScannedFileCount)
def handleComparedFileCount(command:ComparedFileCount)
def handleMessage(command: SubmitMessage)
def handleScannedFileCount(command: ScannedFileCount)
def handleComparedFileCount(command: ComparedFileCount)
}
/**
@ -82,14 +86,14 @@ class DefaultLoggingEngineListener extends EngineListener with ActorLogging {
if (command.message != null) {
log.info(command.message)
}
log.info("Processed {}/{}",command.count,command.total)
log.info("Processed {}/{}", command.count, command.total)
}
override def handleScannedFileCount(command: ScannedFileCount): Unit = {
if (command.message != null) {
log.info(command.message)
}
log.info("Scanned {}/{} For Similarities",command.count,command.total)
log.info("Scanned {}/{} For Similarities", command.count, command.total)
}
override def handleMessage(command: SubmitMessage): Unit = {

19
engine/src/main/scala/com/sothr/imagetools/engine/SequentialEngine.scala

@ -3,8 +3,7 @@ package com.sothr.imagetools.engine
import java.io.File
import akka.actor.{ActorRef, Props}
import com.sothr.imagetools.engine.image.{SimilarImages, ImageService, Image}
import com.sothr.imagetools.image.SimilarImages
import com.sothr.imagetools.engine.image.{Image, ImageService, SimilarImages}
import grizzled.slf4j.Logging
import scala.collection.mutable
@ -30,17 +29,17 @@ class SequentialEngine extends Engine with Logging {
this.similarityListener = listenerRef
}
def getImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[Image] = {
def getImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[Image] = {
debug(s"Looking for images in directory: $directoryPath")
val images:mutable.MutableList[Image] = new mutable.MutableList[Image]()
val images: mutable.MutableList[Image] = new mutable.MutableList[Image]()
val imageFiles = getAllImageFiles(directoryPath, recursive, recursiveDepth)
val directory:File = new File(directoryPath)
val directory: File = new File(directoryPath)
var count = 0
for (file <- imageFiles) {
count += 1
if (count % 25 == 0) {
//info(s"Processed ${count}/${imageFiles.size}")
processedListener ! ScannedFileCount(count,imageFiles.size)
processedListener ! ScannedFileCount(count, imageFiles.size)
}
val image = ImageService.getImage(file)
if (image != null) {
@ -50,7 +49,7 @@ class SequentialEngine extends Engine with Logging {
images.toList
}
def getSimilarImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[SimilarImages] = {
def getSimilarImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[SimilarImages] = {
debug(s"Looking for similar images in directory: $directoryPath")
val images = getImagesForDirectory(directoryPath, recursive, recursiveDepth)
info(s"Searching ${images.length} images for similarities")
@ -61,9 +60,9 @@ class SequentialEngine extends Engine with Logging {
for (rootImage <- images) {
if (!ignoreSet.contains(rootImage)) {
if (processedCount % 25 == 0) {
//info(s"Processed ${processedCount}/${images.length - ignoreSet.size} About ${images.length -
// processedCount} images to go")
similarityListener ! ScannedFileCount(processedCount,images.length-ignoreSet.size)
//info(s"Processed ${processedCount}/${images.length - ignoreSet.size} About ${images.length -
// processedCount} images to go")
similarityListener ! ScannedFileCount(processedCount, images.length - ignoreSet.size)
}
debug(s"Looking for images similar to: ${rootImage.imagePath}")
ignoreSet += rootImage

16
engine/src/main/scala/com/sothr/imagetools/engine/dao/HibernateUtil.scala

@ -14,10 +14,10 @@ import org.hibernate.service.ServiceRegistry
*/
object HibernateUtil extends Logging {
private val sessionFactory:SessionFactory = buildSessionFactory()
private var serviceRegistry:ServiceRegistry = null
private val sessionFactory: SessionFactory = buildSessionFactory()
private var serviceRegistry: ServiceRegistry = null
private def buildSessionFactory():SessionFactory = {
private def buildSessionFactory(): SessionFactory = {
try {
// Create the SessionFactory from hibernate.cfg.xml
val configuration = new Configuration().configure("hibernate.cfg.xml")
@ -27,14 +27,14 @@ object HibernateUtil extends Logging {
serviceRegistry = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties).build
configuration.buildSessionFactory(serviceRegistry)
} catch {
case ex:Throwable =>
// Make sure you log the exception, as it might be swallowed
error("Initial SessionFactory creation failed.", ex)
throw new ExceptionInInitializerError(ex)
case ex: Throwable =>
// Make sure you log the exception, as it might be swallowed
error("Initial SessionFactory creation failed.", ex)
throw new ExceptionInInitializerError(ex)
}
}
def getSessionFactory:SessionFactory = {
def getSessionFactory: SessionFactory = {
sessionFactory
}

14
engine/src/main/scala/com/sothr/imagetools/engine/dao/ImageDAO.scala

@ -10,25 +10,25 @@ import org.hibernate.{Session, SessionFactory}
*/
class ImageDAO {
private val sessionFactory:SessionFactory = HibernateUtil.getSessionFactory
private val sessionFactory: SessionFactory = HibernateUtil.getSessionFactory
def find(path:String):Image = {
val session:Session = sessionFactory.getCurrentSession
def find(path: String): Image = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
val result = session.get(classOf[Image], path).asInstanceOf[Image]
session.getTransaction.commit()
result
}
def save(image:Image) = {
val session:Session = sessionFactory.getCurrentSession
def save(image: Image) = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
session.saveOrUpdate(image)
session.getTransaction.commit()
}
def save(images:List[Image]) = {
val session:Session = sessionFactory.getCurrentSession
def save(images: List[Image]) = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
for (image <- images) session.saveOrUpdate(image)
session.getTransaction.commit()

53
engine/src/main/scala/com/sothr/imagetools/engine/dto/ImageHashDTO.scala

@ -6,30 +6,49 @@ import grizzled.slf4j.Logging
@Entity
@Table(name = "ImageHash")
class ImageHashDTO(var ahash:Long, var dhash:Long, var phash:Long, var md5:String) extends Serializable with Logging {
class ImageHashDTO(var ahash: Long, var dhash: Long, var phash: Long, var md5: String) extends Serializable with Logging {
def this() = this (0l, 0l, 0l, "")
def this() = this(0l, 0l, 0l, "")
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var id:Int = _
def getId:Int = id
def setId(newId:Int) = { id = newId }
var id: Int = _
def getAhash:Long = ahash
def setAhash(hash:Long) = { ahash = hash}
def getDhash:Long = dhash
def setDhash(hash:Long) = { dhash = hash}
def getPhash:Long = phash
def setPhash(hash:Long) = { phash = hash}
def getMd5:String = md5
def setMd5(hash:String) = { md5 = hash}
def getId: Int = id
def cloneHashes:ImageHashDTO = {
new ImageHashDTO(ahash,dhash,phash,md5)
def setId(newId: Int) = {
id = newId
}
override def hashCode():Int = {
def getAhash: Long = ahash
def setAhash(hash: Long) = {
ahash = hash
}
def getDhash: Long = dhash
def setDhash(hash: Long) = {
dhash = hash
}
def getPhash: Long = phash
def setPhash(hash: Long) = {
phash = hash
}
def getMd5: String = md5
def setMd5(hash: String) = {
md5 = hash
}
def cloneHashes: ImageHashDTO = {
new ImageHashDTO(ahash, dhash, phash, md5)
}
override def hashCode(): Int = {
var result = 365
result = 41 * result + (this.ahash ^ (this.ahash >>> 32)).toInt
result = 37 * result + (this.dhash ^ (this.dhash >>> 32)).toInt
@ -37,7 +56,7 @@ class ImageHashDTO(var ahash:Long, var dhash:Long, var phash:Long, var md5:Strin
result
}
override def toString:String = {
override def toString: String = {
s"MD5: $md5 ahash: $ahash dhash: $dhash phash: $phash"
}
}

78
engine/src/main/scala/com/sothr/imagetools/engine/hash/AHash.scala

@ -3,46 +3,46 @@ package com.sothr.imagetools.engine.hash
import grizzled.slf4j.Logging
/**
* Created by dev on 1/22/14.
*/
* Created by dev on 1/22/14.
*/
object AHash extends PerceptualHasher with Logging {
def getHash(imageData: Array[Array[Int]]): Long = {
//debug("Generating AHash")
val width = imageData.length
val height = imageData(0).length
//debug(s"Image data size: ${width}x${height}")
def getHash(imageData: Array[Array[Int]]): Long = {
//debug("Generating AHash")
val width = imageData.length
val height = imageData(0).length
//debug(s"Image data size: ${width}x${height}")
//calculate average pixel
var total = 0
for (row <- 0 until height) {
for (col <- 0 until width) {
total += imageData(row)(col)
}
}
val mean = total / (height * width)
//calculate average pixel
var total = 0
for (row <- 0 until height) {
for (col <- 0 until width) {
total += imageData(row)(col)
}
}
val mean = total / (height * width)
//calculate ahash
var hash = 0L
for (row <- 0 until height by 2) {
//process each column
for (col <- 0 until width by 1) {
hash <<= 1
val pixel = imageData(row)(col)
//If the current pixel is at or above the mean, store it as a one, else store it as a zero
if (pixel >= mean) hash |= 1 else hash |= 0
}
//calculate ahash
var hash = 0L
for (row <- 0 until height by 2) {
//process each column
for (col <- 0 until width by 1) {
hash <<= 1
val pixel = imageData(row)(col)
//If the current pixel is at or above the mean, store it as a one, else store it as a zero
if (pixel >= mean) hash |= 1 else hash |= 0
}
if ((row +1) < width) {
val nextRow = row + 1
//process each column
for (col <- (width - 1) to 0 by -1) {
hash <<= 1
val pixel = imageData(nextRow)(col)
if (pixel >= mean) hash |= 1 else hash |= 0
}
}
}
//debug(s"Computed AHash: $hash from ${width * height} pixels")
hash
}
}
if ((row + 1) < width) {
val nextRow = row + 1
//process each column
for (col <- (width - 1) to 0 by -1) {
hash <<= 1
val pixel = imageData(nextRow)(col)
if (pixel >= mean) hash |= 1 else hash |= 0
}
}
}
//debug(s"Computed AHash: $hash from ${width * height} pixels")
hash
}
}

38
engine/src/main/scala/com/sothr/imagetools/engine/hash/DHash.scala

@ -13,16 +13,16 @@ object DHash extends PerceptualHasher with Logging {
val width = imageData.length
val height = imageData(0).length
//debug(s"Image data size: ${width}x${height}")
//calculate dhash
var hash = 0L
var previousPixel = imageData(height-1)(width-1)
var previousLocation = (height-1, width-1)
var previousPixel = imageData(height - 1)(width - 1)
var previousLocation = (height - 1, width - 1)
if (height % 2 == 0) {
previousPixel = imageData(height-1)(0)
previousLocation = (height-1, 0)
previousPixel = imageData(height - 1)(0)
previousLocation = (height - 1, 0)
}
for (row <- 0 until height by 2) {
//process each column
for (col <- 0 until width by 1) {
@ -36,19 +36,19 @@ object DHash extends PerceptualHasher with Logging {
previousPixel = pixel
previousLocation = (row, col)
}
if ((row +1) < width) {
val nextRow = row + 1
//process each column
for (col <- (width - 1) to 0 by -1) {
hash <<= 1
val pixel = imageData(nextRow)(col)
//debug(s"previousPixel: $previousPixel currentPixel: $pixel previousLocation: $previousLocation currentLocation: (${nextRow},${col})")
if (pixel >= previousPixel) hash |= 1 else hash |= 0
//debug(s"(${row},${col})=$pixel hash=${hash.toBinaryString}")
previousPixel = pixel
previousLocation = (nextRow, col)
}
if ((row + 1) < width) {
val nextRow = row + 1
//process each column
for (col <- (width - 1) to 0 by -1) {
hash <<= 1
val pixel = imageData(nextRow)(col)
//debug(s"previousPixel: $previousPixel currentPixel: $pixel previousLocation: $previousLocation currentLocation: (${nextRow},${col})")
if (pixel >= previousPixel) hash |= 1 else hash |= 0
//debug(s"(${row},${col})=$pixel hash=${hash.toBinaryString}")
previousPixel = pixel
previousLocation = (nextRow, col)
}
}
}
//debug(s"Computed DHash: $hash from ${width * height} pixels")

80
engine/src/main/scala/com/sothr/imagetools/engine/hash/HashService.scala

@ -18,18 +18,18 @@ import resource._
*/
object HashService extends Logging {
def getImageHashes(imagePath:String):ImageHashDTO = {
def getImageHashes(imagePath: String): ImageHashDTO = {
//debug(s"Creating hashes for $imagePath")
getImageHashes(ImageIO.read(new File(imagePath)), imagePath)
}
def getImageHashes(image:BufferedImage, imagePath:String):ImageHashDTO = {
def getImageHashes(image: BufferedImage, imagePath: String): ImageHashDTO = {
//debug("Creating hashes for an image")
var ahash:Long = 0L
var dhash:Long = 0L
var phash:Long = 0L
val md5:String = getMD5(imagePath)
var ahash: Long = 0L
var dhash: Long = 0L
var phash: Long = 0L
val md5: String = getMD5(imagePath)
//Get Image Data
val grayImage = ImageService.convertToGray(image)
@ -50,9 +50,9 @@ object HashService extends Logging {
hashes
}
def getAhash(image:BufferedImage, alreadyGray:Boolean = false):Long = {
def getAhash(image: BufferedImage, alreadyGray: Boolean = false): Long = {
//debug("Started generating an AHash")
var grayImage:BufferedImage = null
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
@ -63,9 +63,9 @@ object HashService extends Logging {
AHash.getHash(imageData)
}
def getDhash(image:BufferedImage, alreadyGray:Boolean = false):Long = {
def getDhash(image: BufferedImage, alreadyGray: Boolean = false): Long = {
//debug("Started generating an DHash")
var grayImage:BufferedImage = null
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
@ -76,9 +76,9 @@ object HashService extends Logging {
DHash.getHash(imageData)
}
def getPhash(image:BufferedImage, alreadyGray:Boolean = false):Long = {
def getPhash(image: BufferedImage, alreadyGray: Boolean = false): Long = {
//debug("Started generating an PHash")
var grayImage:BufferedImage = null
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
@ -89,35 +89,35 @@ object HashService extends Logging {
PHash.getHash(imageData)
}
def getMD5(filePath:String):String = {
def getMD5(filePath: String): String = {
managed(new FileInputStream(filePath)) acquireAndGet {
input =>
DigestUtils.md5Hex(input)
DigestUtils.md5Hex(input)
}
}
def areAhashSimilar(ahash1:Long, ahash2:Long):Boolean = {
def areAhashSimilar(ahash1: Long, ahash2: Long): Boolean = {
val tolerence = PropertiesService.aHashTolerance
val distance = Hamming.getDistance(ahash1, ahash2)
//debug(s"hash1: $ahash1 hash2: $ahash2 tolerence: $tolerence hamming distance: $distance")
if (distance <= tolerence) true else false
}
def areDhashSimilar(dhash1:Long, dhash2:Long):Boolean = {
def areDhashSimilar(dhash1: Long, dhash2: Long): Boolean = {
val tolerence = PropertiesService.dHashTolerance
val distance = Hamming.getDistance(dhash1, dhash2)
//debug(s"hash1: $dhash1 hash2: $dhash2 tolerence: $tolerence hamming distance: $distance")
if (distance <= tolerence) true else false
}
def arePhashSimilar(phash1:Long, phash2:Long):Boolean = {
def arePhashSimilar(phash1: Long, phash2: Long): Boolean = {
val tolerence = PropertiesService.pHashTolerance
val distance = Hamming.getDistance(phash1, phash2)
//debug(s"hash1: $phash1 hash2: $phash2 tolerence: $tolerence hamming distance: $distance")
if (distance <= tolerence) true else false
}
def getWeightedHashSimilarity(imageHash1:ImageHashDTO, imageHash2:ImageHashDTO):Float = {
def getWeightedHashSimilarity(imageHash1: ImageHashDTO, imageHash2: ImageHashDTO): Float = {
//ahash
val aHashTolerance = PropertiesService.aHashTolerance
val aHashWeight = PropertiesService.aHashWeight
@ -132,36 +132,33 @@ object HashService extends Logging {
val usePhash = PropertiesService.useAhash
//calculate weighted values
var weightedHammingTotal:Float = 0
var weightedHammingTotal: Float = 0
var methodsTotal = 0
if (useAhash)
{
if (useAhash) {
val hamming = Hamming.getDistance(imageHash1.ahash, imageHash2.ahash)
weightedHammingTotal += hamming * aHashWeight
//debug(s"hash1: ${imageHash1.ahash} hash2: ${imageHash1.ahash} tolerence: $aHashTolerance hamming distance: $hamming weight: $aHashWeight")
methodsTotal+=1
methodsTotal += 1
}
if (useDhash)
{
if (useDhash) {
val hamming = Hamming.getDistance(imageHash1.dhash, imageHash2.dhash)
weightedHammingTotal += hamming * dHashWeight
//debug(s"hash1: ${imageHash1.dhash} hash2: ${imageHash1.dhash} tolerence: $dHashTolerance hamming distance: $hamming weight: $dHashWeight")
methodsTotal+=1
methodsTotal += 1
}
if (usePhash)
{
if (usePhash) {
val hamming = Hamming.getDistance(imageHash1.phash, imageHash2.phash)
weightedHammingTotal += hamming * pHashWeight
//debug(s"hash1: ${imageHash1.phash} hash2: ${imageHash1.phash} tolerence: $pHashTolerance hamming distance: $hamming weight: $pHashWeight")
methodsTotal+=1
methodsTotal += 1
}
val weightedHammingMean = weightedHammingTotal / methodsTotal
//debug(s"Calculated Weighted Hamming Mean: $weightedHammingMean")
weightedHammingMean
}
def getWeightedHashTolerence:Float = {
def getWeightedHashTolerence: Float = {
//ahash
val aHashTolerance = PropertiesService.aHashTolerance
val aHashWeight = PropertiesService.aHashWeight
@ -176,33 +173,30 @@ object HashService extends Logging {
val usePhash = PropertiesService.useAhash
//calculate weighted values
var weightedToleranceTotal:Float = 0
var weightedToleranceTotal: Float = 0
var methodsTotal = 0
if (useAhash)
{
if (useAhash) {
weightedToleranceTotal += aHashTolerance * aHashWeight
//debug(s"Ahash Tolerance: $aHashTolerance Current Weighted Tolerance: $weightedToleranceTotal")
methodsTotal+=1
methodsTotal += 1
}
if (useDhash)
{
if (useDhash) {
weightedToleranceTotal += dHashTolerance * dHashWeight
//debug(s"Dhash Tolerance: $dHashTolerance Current Weighted Tolerance: $weightedToleranceTotal")
methodsTotal+=1
methodsTotal += 1
}
if (usePhash)
{
if (usePhash) {
weightedToleranceTotal += pHashTolerance * pHashWeight
//debug(s"Phash Tolerance: $pHashTolerance Current Weighted Tolerance: $weightedToleranceTotal")
methodsTotal+=1
methodsTotal += 1
}
val weightedTolerance = weightedToleranceTotal / methodsTotal
//debug(s"Calculated Weighted Tolerance: $weightedTolerance")
weightedTolerance
}
def areImageHashesSimilar(imageHash1:ImageHashDTO, imageHash2:ImageHashDTO):Boolean = {
def areImageHashesSimilar(imageHash1: ImageHashDTO, imageHash2: ImageHashDTO): Boolean = {
val weightedHammingMean = getWeightedHashSimilarity(imageHash1, imageHash2)
val weightedToleranceMean = getWeightedHashTolerence
if (weightedHammingMean <= weightedToleranceMean) true else false

112
engine/src/main/scala/com/sothr/imagetools/engine/hash/PHash.scala

@ -4,66 +4,66 @@ import edu.emory.mathcs.jtransforms.dct.FloatDCT_2D
import grizzled.slf4j.Logging
/**
* Created by dev on 1/22/14.
*/
* Created by dev on 1/22/14.
*/
object PHash extends PerceptualHasher with Logging {
def getHash(imageData: Array[Array[Int]]): Long = {
//convert the imageData into a FloatArray
val width = imageData.length
val height = imageData(0).length
//debug(s"Starting with image of ${height}x${width} for PHash")
def getHash(imageData: Array[Array[Int]]): Long = {
//convert the imageData into a FloatArray
val width = imageData.length
val height = imageData(0).length
//debug(s"Starting with image of ${height}x${width} for PHash")
val imageDataFloat:Array[Array[Float]] = Array.ofDim[Float](height, width)
for (row <- 0 until height) {
for (col <- 0 until width) {
imageDataFloat(row)(col) = imageData(row)(col).toFloat
}
}
//debug("Copied image data to float array for transform")
//debug(s"\n${imageDataFloat.deep.mkString("\n")}")
val imageDataFloat: Array[Array[Float]] = Array.ofDim[Float](height, width)
for (row <- 0 until height) {
for (col <- 0 until width) {
imageDataFloat(row)(col) = imageData(row)(col).toFloat
}
}
//debug("Copied image data to float array for transform")
//debug(s"\n${imageDataFloat.deep.mkString("\n")}")
//perform transform on the data
val dct:FloatDCT_2D = new FloatDCT_2D(height,width)
dct.forward(imageDataFloat, true)
//debug("Converted image data into DCT")
//debug(s"\n${imageDataFloat.deep.mkString("\n")}")
//perform transform on the data
val dct: FloatDCT_2D = new FloatDCT_2D(height, width)
dct.forward(imageDataFloat, true)
//debug("Converted image data into DCT")
//debug(s"\n${imageDataFloat.deep.mkString("\n")}")
//extract the DCT data
val dctDataWidth:Int = width / 4
val dctDataHeight:Int = height / 4
//extract the DCT data
val dctDataWidth: Int = width / 4
val dctDataHeight: Int = height / 4
//calculate the mean
var total = 0.0f
for (row <- 0 until dctDataHeight) {
for (col <- 0 until dctDataWidth) {
total += imageDataFloat(row)(col)
}
}
val mean = total / (dctDataHeight * dctDataWidth)
//debug(s"Calculated mean as $mean from ${total}/${dctDataHeight * dctDataWidth}")
//calculate the mean
var total = 0.0f
for (row <- 0 until dctDataHeight) {
for (col <- 0 until dctDataWidth) {
total += imageDataFloat(row)(col)
}
}
val mean = total / (dctDataHeight * dctDataWidth)
//debug(s"Calculated mean as $mean from ${total}/${dctDataHeight * dctDataWidth}")
//calculate the hash
var hash = 0L
for (row <- 0 until dctDataHeight by 2) {
//process each column
for (col <- 0 until dctDataWidth by 1) {
hash <<= 1
val pixel = imageDataFloat(row)(col)
//If the current pixel is at or above the mean, store it as a one, else store it as a zero
if (pixel >= mean) hash |= 1 else hash |= 0
}
//calculate the hash
var hash = 0L
for (row <- 0 until dctDataHeight by 2) {
//process each column
for (col <- 0 until dctDataWidth by 1) {
hash <<= 1
val pixel = imageDataFloat(row)(col)
//If the current pixel is at or above the mean, store it as a one, else store it as a zero
if (pixel >= mean) hash |= 1 else hash |= 0
}
if ((row +1) < dctDataWidth) {
val nextRow = row + 1
//process each column
for (col <- (dctDataWidth - 1) to 0 by -1) {
hash <<= 1
val pixel = imageDataFloat(nextRow)(col)
if (pixel >= mean) hash |= 1 else hash |= 0
}
}
}
//debug(s"Computed PHash: $hash from ${dctDataWidth * dctDataHeight} pixels")
hash
}
}
if ((row + 1) < dctDataWidth) {
val nextRow = row + 1
//process each column
for (col <- (dctDataWidth - 1) to 0 by -1) {
hash <<= 1
val pixel = imageDataFloat(nextRow)(col)
if (pixel >= mean) hash |= 1 else hash |= 0
}
}
}
//debug(s"Computed PHash: $hash from ${dctDataWidth * dctDataHeight} pixels")
hash
}
}

2
engine/src/main/scala/com/sothr/imagetools/engine/hash/PerceptualHasher.scala

@ -7,6 +7,6 @@ package com.sothr.imagetools.engine.hash
*/
trait PerceptualHasher {
def getHash(imageData:Array[Array[Int]]):Long
def getHash(imageData: Array[Array[Int]]): Long
}

98
engine/src/main/scala/com/sothr/imagetools/engine/image/Image.scala

@ -8,48 +8,74 @@ import grizzled.slf4j.Logging
@Entity
@Table(name = "Image")
class Image(val image:String, val thumbnail:String, val size:(Int, Int), val imageHashes:ImageHashDTO = null) extends Serializable with Logging {
class Image(val image: String, val thumbnail: String, val size: (Int, Int), val imageHashes: ImageHashDTO = null) extends Serializable with Logging {
def this() = this ("", "", (0,0), null)
def this() = this("", "", (0, 0), null)
@Id
var imagePath:String = image
def getImagePath:String = imagePath
def setImagePath(path:String) = { imagePath = path}
var thumbnailPath:String = thumbnail
def getThumbnailPath:String = thumbnailPath
def setThumbnailPath(path:String) = { thumbnailPath = path}
var width:Int = size._1
def getWidth:Int = width
def setWidth(size:Int) = { width = size}
var height:Int = size._2
def getHeight:Int = height
def setHeight(size:Int) = { height = size}
var hashes:ImageHashDTO = imageHashes
def getHashes:ImageHashDTO = hashes
def setHashes(newHashes:ImageHashDTO) = { hashes = newHashes}
var imagePath: String = image
def getImagePath: String = imagePath
def setImagePath(path: String) = {
imagePath = path
}
var thumbnailPath: String = thumbnail
def getThumbnailPath: String = thumbnailPath
def setThumbnailPath(path: String) = {
thumbnailPath = path
}
var width: Int = size._1
def getWidth: Int = width
def setWidth(size: Int) = {
width = size
}
var height: Int = size._2
def getHeight: Int = height
def setHeight(size: Int) = {
height = size
}
var hashes: ImageHashDTO = imageHashes
def getHashes: ImageHashDTO = hashes
def setHashes(newHashes: ImageHashDTO) = {
hashes = newHashes
}
@transient
var imageSize:(Int, Int) = { new Tuple2(width, height) }
var imageSize: (Int, Int) = {
new Tuple2(width, height)
}
@transient
var imageName:String = ""
var imageName: String = ""
var imageType:ImageType = ImageType.SingleFrameImage
var imageType: ImageType = ImageType.SingleFrameImage
def getName:String = {
if(this.imageName.length < 1) {
def getName: String = {
if (this.imageName.length < 1) {
this.imageName = this.getImagePath.split('/').last
}
this.imageName
}
def isSimilarTo(otherImage:Image):Boolean = {
def isSimilarTo(otherImage: Image): Boolean = {
//debug(s"Checking $imagePath for similarities with ${otherImage.imagePath}")
HashService.areImageHashesSimilar(this.hashes,otherImage.hashes)
HashService.areImageHashesSimilar(this.hashes, otherImage.hashes)
}
def getSimilarity(otherImage:Image):Float = {
def getSimilarity(otherImage: Image): Float = {
HashService.getWeightedHashSimilarity(this.hashes, otherImage.hashes)
}
@ -57,27 +83,27 @@ class Image(val image:String, val thumbnail:String, val size:(Int, Int), val ima
}*/
def cloneImage:Image = {
new Image(imagePath,thumbnailPath,imageSize,hashes.cloneHashes)
def cloneImage: Image = {
new Image(imagePath, thumbnailPath, imageSize, hashes.cloneHashes)
}
override def toString:String = {
override def toString: String = {
s"Image: $imagePath Thumbnail: $thumbnailPath Image Size: ${imageSize._1}x${imageSize._2} Hashes: $hashes"
}
override def equals(obj:Any) = {
obj match {
case that:Image =>
that.hashCode.equals(this.hashCode)
case _ => false
}
override def equals(obj: Any) = {
obj match {
case that: Image =>
that.hashCode.equals(this.hashCode)
case _ => false
}
}
override def hashCode:Int = {
override def hashCode: Int = {
var result = 365
result = 37 * result + imagePath.hashCode
result = 41 * result + hashes.hashCode()
result
}
}

4
engine/src/main/scala/com/sothr/imagetools/engine/image/ImageFilter.scala

@ -13,11 +13,11 @@ import scala.collection.immutable.HashSet
*/
class ImageFilter extends FilenameFilter {
private val extensions:HashSet[String] = new HashSet[String]() ++ Array("png", "bmp", "gif", "jpg", "jpeg")
private val extensions: HashSet[String] = new HashSet[String]() ++ Array("png", "bmp", "gif", "jpg", "jpeg")
def accept(dir: File, name: String): Boolean = {
val splitName = name.split('.')
val extension = if (splitName.length > 1) splitName(splitName.length-1) else ""
val extension = if (splitName.length > 1) splitName(splitName.length - 1) else ""
if (extensions.contains(extension)) true else false
}
}

67
engine/src/main/scala/com/sothr/imagetools/engine/image/ImageService.scala

@ -3,6 +3,7 @@ package com.sothr.imagetools.engine.image
import java.awt.image.{BufferedImage, ColorConvertOp, DataBufferByte}
import java.io.{File, IOException}
import javax.imageio.ImageIO
import com.sothr.imagetools.engine.AppConfig
import com.sothr.imagetools.engine.dao.ImageDAO
import com.sothr.imagetools.engine.hash.HashService
@ -16,8 +17,8 @@ object ImageService extends Logging {
val imageCache = AppConfig.cacheManager.getCache("images")
private val imageDAO = new ImageDAO()
private def lookupImage(file:File):Image = {
var image:Image = null
private def lookupImage(file: File): Image = {
var image: Image = null
var found = false
//get from memory cache if possible
try {
@ -26,7 +27,7 @@ object ImageService extends Logging {
found = true
}
} catch {
case npe:NullPointerException => debug(s"\'${file.getAbsolutePath}\' was supposed to be in the cache, but was not")
case npe: NullPointerException => debug(s"\'${file.getAbsolutePath}\' was supposed to be in the cache, but was not")
}
//get from datastore if possible
if (!found) {
@ -34,25 +35,25 @@ object ImageService extends Logging {
val tempImage = imageDAO.find(file.getAbsolutePath)
if (tempImage != null) image = tempImage
} catch {
case ex:Exception => error(s"Error looking up \'${file.getAbsolutePath}\' was supposed to be in the database, but was not", ex)
case ex: Exception => error(s"Error looking up \'${file.getAbsolutePath}\' was supposed to be in the database, but was not", ex)
}
}
image
}
private def saveImage(image:Image):Image = {
private def saveImage(image: Image): Image = {
//save to cache
imageCache.put(new Element(image.imagePath, image))
//save to datastore
try {
imageDAO.save(image)
} catch {
case ex:Exception => error(s"Error saving \'${image.imagePath}\' to database", ex)
case ex: Exception => error(s"Error saving \'${image.imagePath}\' to database", ex)
}
image
}
def getImage(file:File):Image = {
def getImage(file: File): Image = {
try {
val image = lookupImage(file)
if (image != null) {
@ -64,34 +65,36 @@ object ImageService extends Logging {
val hashes = HashService.getImageHashes(bufferedImage, file.getAbsolutePath)
var thumbnailPath = lookupThumbnailPath(hashes.md5)
if (thumbnailPath == null) thumbnailPath = getThumbnail(bufferedImage, hashes.md5)
val imageSize = { (bufferedImage.getWidth, bufferedImage.getHeight) }
val imageSize = {
(bufferedImage.getWidth, bufferedImage.getHeight)
}
val image = new Image(file.getAbsolutePath, thumbnailPath, imageSize, hashes)
debug(s"Created image: $image")
return saveImage(image)
}
} catch {
case ioe:IOException => error(s"Error processing ${file.getAbsolutePath}", ioe)
case ex:Exception => error(s"Error processing ${file.getAbsolutePath}", ex)
case ioe: IOException => error(s"Error processing ${file.getAbsolutePath}", ioe)
case ex: Exception => error(s"Error processing ${file.getAbsolutePath}", ex)
}
null
}
def calculateThumbPath(md5:String):String = {
def calculateThumbPath(md5: String): String = {
//break the path down into 4 char parts
val subPath = md5.substring(0, 3)
var path:String = s"${PropertiesService.get(PropertyEnum.ThumbnailDirectory.toString)}${PropertiesService.get(PropertyEnum.ThumbnailSize.toString)}/$subPath/"
var path: String = s"${PropertiesService.get(PropertyEnum.ThumbnailDirectory.toString)}${PropertiesService.get(PropertyEnum.ThumbnailSize.toString)}/$subPath/"
try {
val dir = new File(path)
if (!dir.exists()) dir.mkdirs()
} catch {
case ioe:IOException => error(s"Unable to create dirs for path: \'$path\'", ioe)
case ioe: IOException => error(s"Unable to create dirs for path: \'$path\'", ioe)
}
path += md5 + ".jpg"
path
}
def lookupThumbnailPath(md5:String):String = {
var thumbPath:String = null
def lookupThumbnailPath(md5: String): String = {
var thumbPath: String = null
if (md5 != null) {
//check for the actual file
val checkPath = calculateThumbPath(md5)
@ -102,9 +105,9 @@ object ImageService extends Logging {
thumbPath
}
def getThumbnail(image:BufferedImage, md5:String):String = {
def getThumbnail(image: BufferedImage, md5: String): String = {
//create thumbnail
val thumb = resize(image, PropertiesService.get(PropertyEnum.ThumbnailSize.toString).toInt, forced=false)
val thumb = resize(image, PropertiesService.get(PropertyEnum.ThumbnailSize.toString).toInt, forced = false)
//calculate path
val path = calculateThumbPath(md5)
// save thumbnail to path
@ -112,7 +115,7 @@ object ImageService extends Logging {
ImageIO.write(thumb, "jpg", new File(path))
debug(s"Wrote thumbnail to $path")
} catch {
case ioe:IOException => error(s"Unable to save thumbnail to $path", ioe)
case ioe: IOException => error(s"Unable to save thumbnail to $path", ioe)
}
// return path
path
@ -121,7 +124,7 @@ object ImageService extends Logging {
/**
* Get the raw data for an image
*/
def getImageData(image:BufferedImage):Array[Array[Int]] = {
def getImageData(image: BufferedImage): Array[Array[Int]] = {
convertTo2DWithoutUsingGetRGB(image)
}
@ -131,10 +134,10 @@ object ImageService extends Logging {
* @param image image to convert to greyscale
* @return
*/
def convertToGray(image:BufferedImage):BufferedImage = {
def convertToGray(image: BufferedImage): BufferedImage = {
//debug("Converting an image to grayscale")
val grayImage = new BufferedImage(image.getWidth, image.getHeight, BufferedImage.TYPE_BYTE_GRAY)
//create a color conversion operation
val op = new ColorConvertOp(
image.getColorModel.getColorSpace,
@ -142,19 +145,19 @@ object ImageService extends Logging {
//convert the image to grey
val result = op.filter(image, grayImage)
//val g = image.getGraphics
//g.drawImage(image,0,0,null)
//g.dispose()
result
}
def resize(image:BufferedImage, size:Int, forced:Boolean=false):BufferedImage = {
def resize(image: BufferedImage, size: Int, forced: Boolean = false): BufferedImage = {
//debug(s"Resizing an image to size: ${size}x${size} forced: $forced")
if (forced) {
Thumbnails.of(image).forceSize(size,size).asBufferedImage
Thumbnails.of(image).forceSize(size, size).asBufferedImage
} else {
Thumbnails.of(image).size(size,size).asBufferedImage
Thumbnails.of(image).size(size, size).asBufferedImage
}
}
@ -164,17 +167,17 @@ object ImageService extends Logging {
* @param image image to convert without using RGB
* @return
*/
private def convertTo2DWithoutUsingGetRGB(image:BufferedImage):Array[Array[Int]] = {
private def convertTo2DWithoutUsingGetRGB(image: BufferedImage): Array[Array[Int]] = {
val pixels = image.getRaster.getDataBuffer.asInstanceOf[DataBufferByte].getData
val numPixels = pixels.length
val width = image.getWidth
val height = image.getHeight
val isSingleChannel = if(numPixels == (width * height)) true else false
val isSingleChannel = if (numPixels == (width * height)) true else false
val hasAlphaChannel = image.getAlphaRaster != null
//debug(s"Converting image to 2d. width:$width height:$height")
val result = Array.ofDim[Int](height,width)
val result = Array.ofDim[Int](height, width)
if (isSingleChannel) {
//debug(s"Processing Single Channel Image")
val pixelLength = 1
@ -183,7 +186,7 @@ object ImageService extends Logging {
//debug(s"Processing pixels 0 until $numPixels by $pixelLength")
for (pixel <- 0 until numPixels by pixelLength) {
//debug(s"Processing pixel: $pixel/${numPixels - 1}")
val argb:Int = pixels(pixel).toInt //singleChannel
val argb: Int = pixels(pixel).toInt //singleChannel
//debug(s"Pixel data: $argb")
result(row)(col) = argb
col += 1
@ -201,7 +204,7 @@ object ImageService extends Logging {
//debug(s"Processing pixels 0 until $numPixels by $pixelLength")
for (pixel <- 0 until numPixels by pixelLength) {
//debug(s"Processing pixel: $pixel/${numPixels - 1}")
var argb:Int = 0
var argb: Int = 0
argb += pixels(pixel).toInt << 24 //alpha
argb += pixels(pixel + 1).toInt //blue
argb += pixels(pixel + 2).toInt << 8 //green
@ -221,7 +224,7 @@ object ImageService extends Logging {
//debug(s"Processing pixels 0 until $numPixels by $pixelLength")
for (pixel <- 0 until numPixels by pixelLength) {
//debug(s"Processing pixel: $pixel/${numPixels - 1}")
var argb:Int = 0
var argb: Int = 0
argb += -16777216; // 255 alpha
argb += pixels(pixel).toInt //blue
argb += pixels(pixel + 1).toInt << 8 //green

22
engine/src/main/scala/com/sothr/imagetools/engine/image/SimilarImages.scala

@ -7,9 +7,9 @@ import grizzled.slf4j.Logging
*
* Created by drew on 1/26/14.
*/
class SimilarImages(val rootImage:Image, val similarImages:List[Image]) extends Logging {
class SimilarImages(val rootImage: Image, val similarImages: List[Image]) extends Logging {
protected def getPrettySimilarImagesList:String = {
protected def getPrettySimilarImagesList: String = {
val sb = new StringBuilder()
for (image <- similarImages) {
sb.append(image.imagePath)
@ -17,17 +17,17 @@ class SimilarImages(val rootImage:Image, val similarImages:List[Image]) extends
}
sb.toString()
}
override def hashCode:Int = {
val prime = 7
var result = prime * 1 + rootImage.hashCode
for (similarImage <- similarImages) {
result = prime * result + similarImage.hashCode
}
result
override def hashCode: Int = {
val prime = 7
var result = prime * 1 + rootImage.hashCode
for (similarImage <- similarImages) {
result = prime * result + similarImage.hashCode
}
result
}
override def toString:String = {
override def toString: String = {
s"""RootImage: ${rootImage.imagePath}
Similar Images:
$getPrettySimilarImagesList""".stripMargin

49
engine/src/main/scala/com/sothr/imagetools/engine/util/PropertiesService.scala

@ -11,15 +11,16 @@ import grizzled.slf4j.Logging
*/
object PropertiesService extends Logging {
private var defaultConf:Config = null
private var userConf:Config = null
private var newUserConf:Properties = new Properties()
private var version:Version = null
def getVersion:Version = this.version
private var defaultConf: Config = null
private var userConf: Config = null
private var newUserConf: Properties = new Properties()
private var version: Version = null
def getVersion: Version = this.version
//specific highly used properties
var TimingEnabled:Boolean = false
var TimingEnabled: Boolean = false
//ahash
var aHashPrecision = 0
var aHashTolerance = 0
@ -39,7 +40,7 @@ object PropertiesService extends Logging {
/*
* Load the properties file from the specified location
*/
def loadProperties(defaultLocation:String, userLocation:String = null) = {
def loadProperties(defaultLocation: String, userLocation: String = null) = {
info(s"Attempting to load properties from: $defaultLocation")
defaultConf = ConfigFactory.load(defaultLocation)
if (userLocation != null) {
@ -50,10 +51,10 @@ object PropertiesService extends Logging {
}
version = new Version(get(PropertyEnum.Version.toString))
info(s"Detected Version: $version")
//load special properties
TimingEnabled = get(PropertyEnum.Timed.toString).toBoolean
//ahash
aHashPrecision = get(PropertyEnum.AhashPrecision.toString).toInt
aHashTolerance = get(PropertyEnum.AhashTolerance.toString).toInt
@ -72,7 +73,7 @@ object PropertiesService extends Logging {
info("Loaded Special Properties")
}
private def cleanAndPrepareNewUserProperties():Properties = {
private def cleanAndPrepareNewUserProperties(): Properties = {
//insert special keys here
newUserConf.setProperty(PropertyEnum.PreviousVersion.toString, version.parsableToString())
//remove special keys here
@ -80,13 +81,13 @@ object PropertiesService extends Logging {
newUserConf
}
private def getCleanedMergedUserConf:Config = {
ConfigFactory.parseProperties(cleanAndPrepareNewUserProperties()) withFallback userConf
private def getCleanedMergedUserConf: Config = {
ConfigFactory.parseProperties(cleanAndPrepareNewUserProperties()) withFallback userConf
}
def saveConf(location:String) = {
def saveConf(location: String) = {
info(s"Saving user properties to $location")
val out:PrintStream = new PrintStream(new FileOutputStream(location, false))
val out: PrintStream = new PrintStream(new FileOutputStream(location, false))
val userConfToSave = getCleanedMergedUserConf
//print to the output stream
out.print(userConfToSave.root.render)
@ -94,34 +95,34 @@ object PropertiesService extends Logging {
out.close()
}
def has(key:String):Boolean = {
def has(key: String): Boolean = {
var result = false
if (newUserConf.containsKey(key)
|| userConf.hasPath(key)
|| defaultConf.hasPath(key)) {
|| userConf.hasPath(key)
|| defaultConf.hasPath(key)) {
result = true
}
result
}
def get(key:String, defaultValue:String=null):String = {
var result:String = defaultValue
def get(key: String, defaultValue: String = null): String = {
var result: String = defaultValue
//check the latest properties
if (newUserConf.containsKey(key)) {
result = newUserConf.getProperty(key)
result = newUserConf.getProperty(key)
}
//check the loaded user properties
else if (userConf.hasPath(key)) {
result = userConf.getString(key)
result = userConf.getString(key)
}
//check the default properties
else if (defaultConf.hasPath(key)) {
result = defaultConf.getString(key)
result = defaultConf.getString(key)
}
result
}
def set(key:String, value:String) = {
def set(key: String, value: String) = {
newUserConf.setProperty(key, value)
}

20
engine/src/main/scala/com/sothr/imagetools/engine/util/Timing.scala

@ -2,34 +2,34 @@ package com.sothr.imagetools.engine.util
import grizzled.slf4j.Logging
trait Timing extends Logging{
def time[R](block: => R): R = {
trait Timing extends Logging {
def time[R](block: => R): R = {
val t0 = System.currentTimeMillis
val result = block // call-by-name
val result = block // call-by-name
val t1 = System.currentTimeMillis
info("Elapsed time: " + (t1 - t0) + "ms")
result
}
def getTime[R](block: => R):Long = {
def getTime[R](block: => R): Long = {
val t0 = System.currentTimeMillis
val result = block // call-by-name
val result = block // call-by-name
val t1 = System.currentTimeMillis
info("Elapsed time: " + (t1 - t0) + "ms")
t1 - t0
}
def getMean(times:Long*):Long = {
def getMean(times: Long*): Long = {
getMean(times.toArray[Long])
}
def getMean(times:Array[Long]):Long = {
def getMean(times: Array[Long]): Long = {
var ag = 0L
for (i <- times.indices) {
ag += times(i)
}
ag / times.length
}
}

46
engine/src/main/scala/com/sothr/imagetools/engine/util/Version.scala

@ -7,18 +7,18 @@ import grizzled.slf4j.Logging
*
* Created by drew on 1/6/14.
*/
class Version(val versionString:String) extends Logging{
class Version(val versionString: String) extends Logging {
//parse version into parts
//typical version string i.e. 0.1.0-DEV-27-060aec7
val (major,minor,patch,buildTag,buildNumber,buildHash) = {
var version:(Int, Int, Int, String, Int, String) = (0,0,0,"DEV",0,"asdfzxcv")
val (major, minor, patch, buildTag, buildNumber, buildHash) = {
var version: (Int, Int, Int, String, Int, String) = (0, 0, 0, "DEV", 0, "asdfzxcv")
try {
val splitVersion = versionString.split("""\.""")
val splitType = splitVersion(splitVersion.length-1).split("""-""")
version = (splitVersion(0).toInt,splitVersion(1).toInt,splitType(0).toInt,splitType(1),splitType(2).toInt,splitType(3))
val splitVersion = versionString.split( """\.""")
val splitType = splitVersion(splitVersion.length - 1).split( """-""")
version = (splitVersion(0).toInt, splitVersion(1).toInt, splitType(0).toInt, splitType(1), splitType(2).toInt, splitType(3))
} catch {
case nfe:NumberFormatException => error(s"Error parsing number from version string '$versionString'", nfe)
case e:Exception => error(s"Unexpected error parsing version string '$versionString'", e)
case nfe: NumberFormatException => error(s"Error parsing number from version string '$versionString'", nfe)
case e: Exception => error(s"Unexpected error parsing version string '$versionString'", e)
}
version
}
@ -33,33 +33,33 @@ class Version(val versionString:String) extends Logging{
* 3 = this.patch > that.patch
* 4 = this.buildTag != that.buildTag
*/
def compare(that:Version):Integer = {
def compare(that: Version): Integer = {
//Identical Versions
if (this.hashCode == that.hashCode) {
0
// This is at least a major version ahead
// This is at least a major version ahead
} else if (this.major > that.major) {
1
// This is at least a major version behind
} else if (this.major < that.major){
// This is at least a major version behind
} else if (this.major < that.major) {
-1
// major is the same
// major is the same
} else {
// This is at least a minor version ahead
if (this.minor > that.minor) {
2
// This is at least a minor version behind
// This is at least a minor version behind
} else if (this.minor < that.minor) {
-2
// major.minor are the same
// major.minor are the same
} else {
// This is at least a patch version ahead
if (this.patch > that.patch) {
3
// This is at least a patch version version
// This is at least a patch version version
} else if (this.patch < that.patch) {
-3
//major.minor.patch are all the same
//major.minor.patch are all the same
} else {
// This is a different build
if (this.buildTag != that.buildTag) {
@ -72,18 +72,18 @@ class Version(val versionString:String) extends Logging{
}
}
def parsableToString():String = {
def parsableToString(): String = {
s"$major.$minor.$patch-$buildTag-$buildNumber-$buildHash"
}
override def toString:String = {
override def toString: String = {
s"$major.$minor.$patch-$buildTag build:$buildNumber code:$buildHash"
}
override def hashCode(): Int = {
val prime:Int = 37
val result:Int = 255
var hash:Int = major
val prime: Int = 37
val result: Int = 255
var hash: Int = major
hash += minor
hash += patch
hash += buildTag.hashCode

46
engine/src/test/java/com/sothr/imagetools/engine/AppTest.java

@ -7,31 +7,27 @@ import junit.framework.TestSuite;
/**
* Unit test for simple App.
*/
public class AppTest extends TestCase
{
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest( String testName )
{
super( testName );
}
public class AppTest extends TestCase {
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest(String testName) {
super(testName);
}
/**
* @return the suite of tests being tested
*/
public static Test suite()
{
return new TestSuite( AppTest.class );
}
/**
* @return the suite of tests being tested
*/
public static Test suite() {
return new TestSuite(AppTest.class);
}
/**
* Rigourous Test :-)
*/
public void testApp()
{
assertTrue( true );
}
/**
* Rigourous Test :-)
*/
public void testApp() {
assertTrue(true);
}
}

2
engine/src/test/resources/application.conf

@ -23,7 +23,7 @@ app {
differenceThreshold = 0.90
//control generation of hashes for new images.
hash {
precision=64
precision = 64
}
ahash {
use = true

9
engine/src/test/resources/hibernate.cfg.xml

@ -22,15 +22,18 @@
<!-- Enable the second-level cache -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
</property>
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="c3p0.acquire_increment">1</property>
<property name="c3p0.idle_test_period">100</property> <!-- seconds -->
<property name="c3p0.idle_test_period">100</property>
<!-- seconds -->
<property name="c3p0.max_size">50</property>
<property name="c3p0.max_statements">0</property>
<property name="c3p0.min_size">5</property>
<property name="c3p0.timeout">100</property> <!-- seconds -->
<property name="c3p0.timeout">100</property>
<!-- seconds -->
<!-- mapping files -->
<mapping resource="hibernate/Image.hbm.xml"/>

3
engine/src/test/resources/hibernate/Image.hbm.xml

@ -11,6 +11,7 @@
<property name="thumbnailPath" column="thumbnail_path" type="string" not-null="true"/>
<property name="width" column="width" type="int" not-null="true"/>
<property name="height" column="height" type="int" not-null="true"/>
<many-to-one name="hashes" column="hashes" unique="true" class="com.sothr.imagetools.engine.dto.ImageHashDTO" cascade="save-update, delete" not-null="true" lazy="false"/>
<many-to-one name="hashes" column="hashes" unique="true" class="com.sothr.imagetools.engine.dto.ImageHashDTO"
cascade="save-update, delete" not-null="true" lazy="false"/>
</class>
</hibernate-mapping>

12
engine/src/test/resources/logback-minimum-config.xml

@ -3,9 +3,9 @@
<appender name="DL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.debug</File>
<encoder>
<withJansi>false</withJansi>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
@ -20,9 +20,9 @@
<appender name="IL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.info</File>
<encoder>
<withJansi>false</withJansi>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
@ -37,9 +37,9 @@
<appender name="EL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<withJansi>false</withJansi>
<pattern>[%.16thread] [%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>

32
engine/src/test/scala/com/sothr/imagetools/engine/EngineTest.scala

@ -5,28 +5,40 @@ package com.sothr.imagetools.engine
*
* Created by drew on 1/26/14.
*/
class EngineTest extends BaseTest{
class EngineTest extends BaseTest {
test("SequentialEngine Test getImagesForDirectory for sample directory") {
val engine:Engine = new SequentialEngine()
assertResult(3) { engine.getImagesForDirectory("sample").length }
val engine: Engine = new SequentialEngine()
assertResult(3) {
engine.getImagesForDirectory("sample").length
}
}
test("SequentialEngine Test getSimilarImagesForDirectory for sample directory") {
val engine = new SequentialEngine()
val similarImages = engine.getSimilarImagesForDirectory("sample")
assertResult(1) { similarImages.length }
assertResult(2) { similarImages(0).similarImages.length }
assertResult(1) {
similarImages.length
}
assertResult(2) {
similarImages(0).similarImages.length
}
}
test("ConcurrentEngine Test getImagesForDirectory for sample directory") {
val engine:Engine = new ConcurrentEngine()
assertResult(3) { engine.getImagesForDirectory("sample").length }
val engine: Engine = new ConcurrentEngine()
assertResult(3) {
engine.getImagesForDirectory("sample").length
}
}
test("ConcurrentEngine Test getSimilarImagesForDirectory for sample directory") {
val engine = new ConcurrentEngine()
val similarImages = engine.getSimilarImagesForDirectory("sample")
assertResult(1) { similarImages.length }
assertResult(2) { similarImages(0).similarImages.length }
assertResult(1) {
similarImages.length
}
assertResult(2) {
similarImages(0).similarImages.length
}
}
}

6
engine/src/test/scala/com/sothr/imagetools/engine/TestParams.scala

@ -1,7 +1,7 @@
package com.sothr.imagetools.engine
object TestParams {
val LargeSampleImage1 = "sample/sample_01_large.jpg"
val MediumSampleImage1 = "sample/sample_01_medium.jpg"
val SmallSampleImage1 = "sample/sample_01_small.jpg"
val LargeSampleImage1 = "sample/sample_01_large.jpg"
val MediumSampleImage1 = "sample/sample_01_medium.jpg"
val SmallSampleImage1 = "sample/sample_01_small.jpg"
}

145
engine/src/test/scala/com/sothr/imagetools/engine/hash/HashServiceTest.scala

@ -3,9 +3,8 @@ package com.sothr.imagetools.engine.hash
import java.io.File
import javax.imageio.ImageIO
import com.sothr.imagetools.engine.{TestParams, BaseTest, AppConfig}
import com.sothr.imagetools.engine.dto.ImageHashDTO
import com.sothr.imagetools.TestParams
import com.sothr.imagetools.engine.{AppConfig, BaseTest, TestParams}
import net.sf.ehcache.Element
import scala.collection.mutable
@ -20,10 +19,10 @@ class HashServiceTest extends BaseTest {
// Define the number of runs the benchmarking tests should use
val benchmarkRuns = 10
def dhashTestCase(filePath:String):Long = {
val sample = new File(filePath)
val image = ImageIO.read(sample)
HashService.getDhash(image)
def dhashTestCase(filePath: String): Long = {
val sample = new File(filePath)
val image = ImageIO.read(sample)
HashService.getDhash(image)
}
test("Benchmark DHash") {
@ -31,21 +30,27 @@ class HashServiceTest extends BaseTest {
info("DHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
time += getTime { dhashTestCase(TestParams.LargeSampleImage1) }
time += getTime {
dhashTestCase(TestParams.LargeSampleImage1)
}
}
val largeMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("DHash Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { dhashTestCase(TestParams.MediumSampleImage1) }
time += getTime {
dhashTestCase(TestParams.MediumSampleImage1)
}
}
val mediumMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("DHash Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { dhashTestCase(TestParams.SmallSampleImage1) }
time += getTime {
dhashTestCase(TestParams.SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
@ -54,15 +59,15 @@ class HashServiceTest extends BaseTest {
}
test("Confirm Largest DHash Output ") {
val testData:Array[Array[Int]] = Array(
Array(1,2,3,4,5,6,7,8),
Array(16,15,14,13,12,11,10,9),
Array(17,18,19,20,21,22,23,24),
Array(32,31,30,29,28,27,26,25),
Array(33,34,35,36,37,38,39,40),
Array(48,47,46,45,44,43,42,41),
Array(49,50,51,52,53,54,55,56),
Array(64,63,62,61,60,59,58,57))
val testData: Array[Array[Int]] = Array(
Array(1, 2, 3, 4, 5, 6, 7, 8),
Array(16, 15, 14, 13, 12, 11, 10, 9),
Array(17, 18, 19, 20, 21, 22, 23, 24),
Array(32, 31, 30, 29, 28, 27, 26, 25),
Array(33, 34, 35, 36, 37, 38, 39, 40),
Array(48, 47, 46, 45, 44, 43, 42, 41),
Array(49, 50, 51, 52, 53, 54, 55, 56),
Array(64, 63, 62, 61, 60, 59, 58, 57))
val hash = DHash.getHash(testData)
debug(s"Hash of test array: $hash")
assert(hash == Long.MaxValue)
@ -78,7 +83,7 @@ class HashServiceTest extends BaseTest {
debug(s"Testing that $hash = 4004374827879799635L")
assert(hash == 4004374827879799635L)
}
test("Calculate DHash Medium Sample Image 1") {
debug("Starting 'Calculate DHash Medium Sample Image 1' test")
val sample = new File(TestParams.MediumSampleImage1)
@ -89,7 +94,7 @@ class HashServiceTest extends BaseTest {
debug(s"Testing that $hash = 4004374827879799635L")
assert(hash == 4004374827879799635L)
}
test("Calculate DHash Small Sample Image 1") {
debug("Starting 'Calculate DHash Small Sample Image 1' test")
val sample = new File(TestParams.SmallSampleImage1)
@ -100,17 +105,17 @@ class HashServiceTest extends BaseTest {
debug(s"Testing that $hash = 4004383623972821843L")
assert(hash == 4004383623972821843L)
}
test("DHash Of Large, Medium, And Small Sample 1 Must Be Similar") {
val largeHash = dhashTestCase(TestParams.LargeSampleImage1)
val mediumHash = dhashTestCase(TestParams.MediumSampleImage1)
val smallHash = dhashTestCase(TestParams.SmallSampleImage1)
assert(HashService.areDhashSimilar(largeHash,mediumHash))
assert(HashService.areDhashSimilar(largeHash,smallHash))
assert(HashService.areDhashSimilar(mediumHash,smallHash))
assert(HashService.areDhashSimilar(largeHash, mediumHash))
assert(HashService.areDhashSimilar(largeHash, smallHash))
assert(HashService.areDhashSimilar(mediumHash, smallHash))
}
def ahashTestCase(filePath:String):Long = {
def ahashTestCase(filePath: String): Long = {
val sample = new File(filePath)
val image = ImageIO.read(sample)
HashService.getAhash(image)
@ -121,21 +126,27 @@ class HashServiceTest extends BaseTest {
info("AHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
time += getTime { ahashTestCase(TestParams.LargeSampleImage1) }
time += getTime {
ahashTestCase(TestParams.LargeSampleImage1)
}
}
val largeMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("AHash Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { ahashTestCase(TestParams.MediumSampleImage1) }
time += getTime {
ahashTestCase(TestParams.MediumSampleImage1)
}
}
val mediumMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("AHash Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { ahashTestCase(TestParams.SmallSampleImage1) }
time += getTime {
ahashTestCase(TestParams.SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
@ -180,12 +191,12 @@ class HashServiceTest extends BaseTest {
val largeHash = ahashTestCase(TestParams.LargeSampleImage1)
val mediumHash = ahashTestCase(TestParams.MediumSampleImage1)
val smallHash = ahashTestCase(TestParams.SmallSampleImage1)
assert(HashService.areAhashSimilar(largeHash,mediumHash))
assert(HashService.areAhashSimilar(largeHash,smallHash))
assert(HashService.areAhashSimilar(mediumHash,smallHash))
assert(HashService.areAhashSimilar(largeHash, mediumHash))
assert(HashService.areAhashSimilar(largeHash, smallHash))
assert(HashService.areAhashSimilar(mediumHash, smallHash))
}
def phashTestCase(filePath:String):Long = {
def phashTestCase(filePath: String): Long = {
val sample = new File(filePath)
val image = ImageIO.read(sample)
HashService.getPhash(image)
@ -196,21 +207,27 @@ class HashServiceTest extends BaseTest {
info("PHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
time += getTime { phashTestCase(TestParams.LargeSampleImage1) }
time += getTime {
phashTestCase(TestParams.LargeSampleImage1)
}
}
val largeMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("PHash Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { phashTestCase(TestParams.MediumSampleImage1) }
time += getTime {
phashTestCase(TestParams.MediumSampleImage1)
}
}
val mediumMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("PHash Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { phashTestCase(TestParams.SmallSampleImage1) }
time += getTime {
phashTestCase(TestParams.SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
@ -255,12 +272,12 @@ class HashServiceTest extends BaseTest {
val largeHash = phashTestCase(TestParams.LargeSampleImage1)
val mediumHash = phashTestCase(TestParams.MediumSampleImage1)
val smallHash = phashTestCase(TestParams.SmallSampleImage1)
assert(HashService.arePhashSimilar(largeHash,mediumHash))
assert(HashService.arePhashSimilar(largeHash,smallHash))
assert(HashService.arePhashSimilar(mediumHash,smallHash))
assert(HashService.arePhashSimilar(largeHash, mediumHash))
assert(HashService.arePhashSimilar(largeHash, smallHash))
assert(HashService.arePhashSimilar(mediumHash, smallHash))
}
def md5TestCase(filePath:String):String = {
def md5TestCase(filePath: String): String = {
HashService.getMD5(filePath)
}
@ -269,21 +286,27 @@ class HashServiceTest extends BaseTest {
info("MD5 Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
time += getTime { md5TestCase(TestParams.LargeSampleImage1) }
time += getTime {
md5TestCase(TestParams.LargeSampleImage1)
}
}
val largeMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("MD5 Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { md5TestCase(TestParams.MediumSampleImage1) }
time += getTime {
md5TestCase(TestParams.MediumSampleImage1)
}
}
val mediumMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("MD5 Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { md5TestCase(TestParams.SmallSampleImage1) }
time += getTime {
md5TestCase(TestParams.SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
@ -312,19 +335,19 @@ class HashServiceTest extends BaseTest {
assert(hash == "b137131bd55896c747286e4d247b845e")
}
def imageHashTestWithCacheCase(filePath:String):ImageHashDTO = {
def imageHashTestWithCacheCase(filePath: String): ImageHashDTO = {
val cache = AppConfig.cacheManager.getCache("images")
var result:ImageHashDTO = null
var result: ImageHashDTO = null
if (cache.get(filePath) != null) {
result = cache.get(filePath).getObjectValue.asInstanceOf[ImageHashDTO]
} else {
result = imageHashTestCase(filePath)
cache.put(new Element(filePath,result))
cache.put(new Element(filePath, result))
}
result
}
def imageHashTestCase(filePath:String):ImageHashDTO = {
def imageHashTestCase(filePath: String): ImageHashDTO = {
HashService.getImageHashes(filePath)
}
@ -333,21 +356,27 @@ class HashServiceTest extends BaseTest {
info("getImageHashes with cache Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
time += getTime { imageHashTestWithCacheCase(TestParams.LargeSampleImage1) }
time += getTime {
imageHashTestWithCacheCase(TestParams.LargeSampleImage1)
}
}
val largeMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("getImageHashes with cache Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { imageHashTestWithCacheCase(TestParams.MediumSampleImage1) }
time += getTime {
imageHashTestWithCacheCase(TestParams.MediumSampleImage1)
}
}
val mediumMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("getImageHashes with cache Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { imageHashTestWithCacheCase(TestParams.SmallSampleImage1) }
time += getTime {
imageHashTestWithCacheCase(TestParams.SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
@ -360,21 +389,27 @@ class HashServiceTest extends BaseTest {
info("getImageHashes Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
time += getTime { imageHashTestCase(TestParams.LargeSampleImage1) }
time += getTime {
imageHashTestCase(TestParams.LargeSampleImage1)
}
}
val largeMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("getImageHashes Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { imageHashTestCase(TestParams.MediumSampleImage1) }
time += getTime {
imageHashTestCase(TestParams.MediumSampleImage1)
}
}
val mediumMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("getImageHashes Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
time += getTime { imageHashTestCase(TestParams.SmallSampleImage1) }
time += getTime {
imageHashTestCase(TestParams.SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
@ -386,9 +421,9 @@ class HashServiceTest extends BaseTest {
val largeHash = imageHashTestCase(TestParams.LargeSampleImage1)
val mediumHash = imageHashTestCase(TestParams.MediumSampleImage1)
val smallHash = imageHashTestCase(TestParams.SmallSampleImage1)
assert(HashService.areImageHashesSimilar(largeHash,mediumHash))
assert(HashService.areImageHashesSimilar(largeHash,smallHash))
assert(HashService.areImageHashesSimilar(mediumHash,smallHash))
assert(HashService.areImageHashesSimilar(largeHash, mediumHash))
assert(HashService.areImageHashesSimilar(largeHash, smallHash))
assert(HashService.areImageHashesSimilar(mediumHash, smallHash))
}
test("Calculate ImageHash Large Sample Image 1") {

22
engine/src/test/scala/com/sothr/imagetools/engine/image/ImageFilterTest.scala

@ -9,10 +9,10 @@ import com.sothr.imagetools.engine.BaseTest
*
* Created by drew on 1/26/14.
*/
class ImageFilterTest extends BaseTest{
class ImageFilterTest extends BaseTest {
test("Confirm ImageFilter Works") {
val filter:ImageFilter = new ImageFilter()
val filter: ImageFilter = new ImageFilter()
val bogusDirectory = new File(".")
assert(filter.accept(bogusDirectory, "test.png"))
assert(filter.accept(bogusDirectory, "test.bmp"))
@ -23,12 +23,20 @@ class ImageFilterTest extends BaseTest{
}
test("Confirm ImageFiler Fails") {
val filter:ImageFilter = new ImageFilter()
val filter: ImageFilter = new ImageFilter()
val bogusDirectory = new File(".")
assertResult(false) { filter.accept(bogusDirectory,"test") }
assertResult(false) { filter.accept(bogusDirectory,"test.mp4") }
assertResult(false) { filter.accept(bogusDirectory,"test.gif.mp4") }
assertResult(false) { filter.accept(bogusDirectory,"") }
assertResult(false) {
filter.accept(bogusDirectory, "test")
}
assertResult(false) {
filter.accept(bogusDirectory, "test.mp4")
}
assertResult(false) {
filter.accept(bogusDirectory, "test.gif.mp4")
}
assertResult(false) {
filter.accept(bogusDirectory, "")
}
}
}

34
gui/pom.xml

@ -1,5 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@ -27,16 +27,16 @@
<artifactId>ImageTools-Engine</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
@ -84,18 +84,18 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>prepare</id>
<phase>process-resources</phase>
<configuration>
<phase>process-resources</phase>
<configuration>
<tasks>
<copy file="${project.build.directory}/version.info" toFile="${basedir}/version.info" overwrite="true" />
<copy file="${project.build.directory}/name.info" toFile="${basedir}/name.info" overwrite="true" />
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE" overwrite="true" />
<copy file="${project.build.directory}/version.info" toFile="${basedir}/version.info" overwrite="true"/>
<copy file="${project.build.directory}/name.info" toFile="${basedir}/name.info" overwrite="true"/>
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE" overwrite="true"/>
<chmod file="${project.build.directory}/startGUI.sh" perm="755"/>
</tasks>
</configuration>
@ -116,7 +116,7 @@
<goal>run</goal>
</goals>
</execution>
</executions>
</executions>
</plugin>
</plugins>
</build>

18
gui/src/includes/logback.xml

@ -4,10 +4,10 @@
<logger name="net.sf.ehcache" level="WARN"/>
<appender name="C" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Sorry Windows Users -->
<withJansi>false</withJansi>
<!-- Sorry Windows Users -->
<withJansi>false</withJansi>
<pattern>[%date{HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
@ -16,9 +16,9 @@
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.debug</File>
<encoder>
<withJansi>false</withJansi>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
@ -34,9 +34,9 @@
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.info</File>
<encoder>
<withJansi>false</withJansi>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
@ -52,9 +52,9 @@
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<withJansi>false</withJansi>
<pattern>[%.16thread] [%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>

114
gui/src/main/java/com/sothr/imagetools/ui/App.java

@ -17,71 +17,69 @@ import java.util.List;
/**
* Image Tools
*/
public class App extends Application
{
public class App extends Application {
private static Logger logger;
private static Logger logger;
private static final String MAINGUI_FXML = "fxml/mainapp/MainApp.fxml";
private static final String MAINGUI_FXML = "fxml/mainapp/MainApp.fxml";
public static void main( String[] args )
{
AppConfig.configureApp();
public static void main(String[] args) {
AppConfig.configureApp();
try {
//try to run the UI
launch(args);
} catch (Exception ex) {
logger.error("A fatal error has occurred: ", ex);
//show popup about the error to the user then exit
}
try {
//try to run the UI
launch(args);
} catch (Exception ex) {
logger.error("A fatal error has occurred: ", ex);
//show popup about the error to the user then exit
}
}
@Override
public void init() throws Exception{
AppConfig.configureApp();
logger = LoggerFactory.getLogger(this.getClass());
logger.info("Initializing Image Tools");
List<String> parameters = this.getParameters().getRaw();
logger.info(String.format("Application was called with '%s' parameters", parameters.toString()));
super.init();
}
@Override
public void init() throws Exception {
AppConfig.configureApp();
logger = LoggerFactory.getLogger(this.getClass());
logger.info("Initializing Image Tools");
List<String> parameters = this.getParameters().getRaw();
logger.info(String.format("Application was called with '%s' parameters", parameters.toString()));
super.init();
}
@Override
public void start(Stage primaryStage) throws Exception {
logger.info("Image-Tools is starting");
logger.info(String.format("Launching GUI with FXML file %s", MAINGUI_FXML));
//store the primary stage globally for reference in popups and the like
AppConfig.setPrimaryStage(primaryStage);
try {
Parent root = FXMLLoader.load(ResourceLoader.get().getResource(MAINGUI_FXML));
primaryStage.setScene(new Scene(root));
//config main scene
primaryStage.setTitle("Image Tools");
primaryStage.setMinHeight(600.0);
primaryStage.setMinWidth(800.0);
primaryStage.setResizable(true);
//show main scene
primaryStage.show();
} catch (IOException ioe) {
String message = String.format("Unable to load FXML file: %s", MAINGUI_FXML);
ImageToolsException ite = new ImageToolsException(message, ioe);
logger.error(message, ioe);
throw ite;
} catch (Exception ex) {
String message = "An unhandled exception was thrown by the GUI";
ImageToolsException ite = new ImageToolsException(message, ex);
logger.error(message, ex);
throw ite;
}
@Override
public void start(Stage primaryStage) throws Exception {
logger.info("Image-Tools is starting");
logger.info(String.format("Launching GUI with FXML file %s", MAINGUI_FXML));
//store the primary stage globally for reference in popups and the like
AppConfig.setPrimaryStage(primaryStage);
try {
Parent root = FXMLLoader.load(ResourceLoader.get().getResource(MAINGUI_FXML));
primaryStage.setScene(new Scene(root));
//config main scene
primaryStage.setTitle("Image Tools");
primaryStage.setMinHeight(600.0);
primaryStage.setMinWidth(800.0);
primaryStage.setResizable(true);
//show main scene
primaryStage.show();
} catch (IOException ioe) {
String message = String.format("Unable to load FXML file: %s", MAINGUI_FXML);
ImageToolsException ite = new ImageToolsException(message, ioe);
logger.error(message, ioe);
throw ite;
} catch (Exception ex) {
String message = "An unhandled exception was thrown by the GUI";
ImageToolsException ite = new ImageToolsException(message, ex);
logger.error(message, ex);
throw ite;
}
}
@Override
public void stop() throws Exception {
logger.info("Image-Tools is shutting down");
AppConfig.shutdown();
super.stop();
//force the JVM to close
System.exit(0);
}
@Override
public void stop() throws Exception {
logger.info("Image-Tools is shutting down");
AppConfig.shutdown();
super.stop();
//force the JVM to close
System.exit(0);
}
}

170
gui/src/main/resources/fxml/mainapp/MainApp.fxml

@ -5,92 +5,116 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<AnchorPane fx:id="rootPane" minHeight="600.0" minWidth="1024.0" prefHeight="600.0" prefWidth="1024.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sothr.imagetools.ui.controller.AppController">
<AnchorPane fx:id="rootPane" minHeight="600.0" minWidth="1024.0" prefHeight="600.0" prefWidth="1024.0"
xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.sothr.imagetools.ui.controller.AppController">
<children>
<MenuBar fx:id="rootMenuBar" minWidth="-Infinity" prefHeight="30.0" prefWidth="600.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<MenuBar fx:id="rootMenuBar" minWidth="-Infinity" prefHeight="30.0" prefWidth="600.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<menus>
<Menu mnemonicParsing="false" text="File">
<items>
<MenuItem fx:id="" mnemonicParsing="false" onAction="#closeAction" text="Close" />
<MenuItem fx:id="" mnemonicParsing="false" onAction="#closeAction" text="Close"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Edit">
<items>
<MenuItem mnemonicParsing="false" text="Settings" />
<MenuItem mnemonicParsing="false" text="Settings"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Help">
<items>
<MenuItem mnemonicParsing="false" onAction="#aboutAction" text="About" />
<MenuItem mnemonicParsing="false" onAction="#helpAction" text="Help Site" />
<MenuItem mnemonicParsing="false" onAction="#aboutAction" text="About"/>
<MenuItem mnemonicParsing="false" onAction="#helpAction" text="Help Site"/>
</items>
</Menu>
</menus>
</MenuBar>
<VBox id="VBox" alignment="CENTER" spacing="5.0" AnchorPane.bottomAnchor="30.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="30.0">
<VBox id="VBox" alignment="CENTER" spacing="5.0" AnchorPane.bottomAnchor="30.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="30.0">
<children>
<SplitPane dividerPositions="0.2181996086105675" focusTraversable="true" prefHeight="569.0" prefWidth="1024.0" visible="true" VBox.vgrow="ALWAYS">
<SplitPane dividerPositions="0.2181996086105675" focusTraversable="true" prefHeight="569.0" prefWidth="1024.0"
visible="true" VBox.vgrow="ALWAYS">
<items>
<TabPane maxWidth="220.0" minHeight="0.0" minWidth="220.0" prefHeight="567.0" prefWidth="220.0" tabClosingPolicy="UNAVAILABLE">
<TabPane maxWidth="220.0" minHeight="0.0" minWidth="220.0" prefHeight="567.0" prefWidth="220.0"
tabClosingPolicy="UNAVAILABLE">
<tabs>
<Tab closable="false" text="Folders">
<content>
<BorderPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="200.0" prefWidth="200.0">
<top>
<Button maxWidth="1.7976931348623157E308" minWidth="200.0" mnemonicParsing="false" onAction="#browseFolders" text="Browse" BorderPane.alignment="CENTER" />
</top>
<center>
<FlowPane prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<children>
<Label maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="210.0" text="Selected Folder:">
<padding>
<Insets left="5.0" right="5.0" />
</padding>
</Label>
<Label fx:id="selectedDirectoryLabel" alignment="TOP_LEFT" lineSpacing="2.0" maxHeight="1.7976931348623157E308" maxWidth="210.0" minWidth="210.0" prefWidth="210.0" text="&lt;SELECTED&gt;" wrapText="true">
<font>
<Font name="System Bold" size="12.0" />
</font>
<padding>
<Insets left="5.0" right="5.0" />
</padding>
</Label>
</children>
</FlowPane>
</center>
<bottom>
<FlowPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="60.0" prefWidth="220.0" BorderPane.alignment="CENTER">
<children>
<Button maxWidth="1.7976931348623157E308" minWidth="220.0" mnemonicParsing="false" onAction="#showAllImages" text="Show All Images">
<FlowPane.margin>
<Insets bottom="5.0" />
</FlowPane.margin>
</Button>
<Button maxWidth="200.0" minWidth="220.0" mnemonicParsing="false" onAction="#showSimilarImages" text="Show Similar Images" />
</children>
</FlowPane>
</bottom>
<padding>
<Insets bottom="5.0" top="5.0" />
</padding>
</BorderPane>
</content>
<content>
<BorderPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="200.0"
prefWidth="200.0">
<top>
<Button maxWidth="1.7976931348623157E308" minWidth="200.0" mnemonicParsing="false"
onAction="#browseFolders" text="Browse" BorderPane.alignment="CENTER"/>
</top>
<center>
<FlowPane prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<children>
<Label maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="210.0"
text="Selected Folder:">
<padding>
<Insets left="5.0" right="5.0"/>
</padding>
</Label>
<Label fx:id="selectedDirectoryLabel" alignment="TOP_LEFT" lineSpacing="2.0"
maxHeight="1.7976931348623157E308" maxWidth="210.0" minWidth="210.0"
prefWidth="210.0" text="&lt;SELECTED&gt;" wrapText="true">
<font>
<Font name="System Bold" size="12.0"/>
</font>
<padding>
<Insets left="5.0" right="5.0"/>
</padding>
</Label>
</children>
</FlowPane>
</center>
<bottom>
<FlowPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="60.0"
prefWidth="220.0" BorderPane.alignment="CENTER">
<children>
<Button maxWidth="1.7976931348623157E308" minWidth="220.0" mnemonicParsing="false"
onAction="#showAllImages" text="Show All Images">
<FlowPane.margin>
<Insets bottom="5.0"/>
</FlowPane.margin>
</Button>
<Button maxWidth="200.0" minWidth="220.0" mnemonicParsing="false"
onAction="#showSimilarImages" text="Show Similar Images"/>
</children>
</FlowPane>
</bottom>
<padding>
<Insets bottom="5.0" top="5.0"/>
</padding>
</BorderPane>
</content>
</Tab>
<Tab text="Tags">
<content>
<AnchorPane id="Content" minHeight="0.0" minWidth="0.0" prefHeight="180.0" prefWidth="200.0">
<children>
<AnchorPane id="AnchorPane" maxHeight="50.0" prefHeight="50.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="5.0">
<AnchorPane id="AnchorPane" maxHeight="50.0" prefHeight="50.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="5.0">
<children>
<TextField layoutY="0.0" prefWidth="200.0" text="" AnchorPane.leftAnchor="20.0" AnchorPane.rightAnchor="20.0" />
<Button layoutY="27.0" mnemonicParsing="false" prefWidth="192.0" text="Filter" AnchorPane.leftAnchor="20.0" AnchorPane.rightAnchor="20.0" />
<TextField layoutY="0.0" prefWidth="200.0" text="" AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
<Button layoutY="27.0" mnemonicParsing="false" prefWidth="192.0" text="Filter"
AnchorPane.leftAnchor="20.0" AnchorPane.rightAnchor="20.0"/>
</children>
</AnchorPane>
<ListView fx:id="tagListView" prefHeight="385.0" prefWidth="198.0" AnchorPane.bottomAnchor="60.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="60.0" />
<AnchorPane id="AnchorPane" prefWidth="192.0" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
<ListView fx:id="tagListView" prefHeight="385.0" prefWidth="198.0"
AnchorPane.bottomAnchor="60.0" AnchorPane.leftAnchor="10.0"
AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="60.0"/>
<AnchorPane id="AnchorPane" prefWidth="192.0" AnchorPane.bottomAnchor="5.0"
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
<children>
<Button layoutY="2.0" mnemonicParsing="false" prefWidth="192.0" text="View All Images In Tags" AnchorPane.leftAnchor="20.0" AnchorPane.rightAnchor="20.0" />
<Button layoutY="28.0" mnemonicParsing="false" prefWidth="192.0" text="Search For Similarities In Tags" AnchorPane.leftAnchor="20.0" AnchorPane.rightAnchor="20.0" />
<Button layoutY="2.0" mnemonicParsing="false" prefWidth="192.0"
text="View All Images In Tags" AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
<Button layoutY="28.0" mnemonicParsing="false" prefWidth="192.0"
text="Search For Similarities In Tags" AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
</children>
</AnchorPane>
</children>
@ -103,20 +127,25 @@
<children>
<ToolBar maxHeight="30.0" minHeight="30.0" prefHeight="30.0" VBox.vgrow="ALWAYS">
<items>
<Label text="Currect Directory:" />
<Separator orientation="VERTICAL" prefHeight="200.0" />
<Label text="&lt;CURRENT DIRECTORY&gt;" />
<Label text="Currect Directory:"/>
<Separator orientation="VERTICAL" prefHeight="200.0"/>
<Label text="&lt;CURRENT DIRECTORY&gt;"/>
</items>
<VBox.margin>
<Insets bottom="-5.0" />
<Insets bottom="-5.0"/>
</VBox.margin>
</ToolBar>
<ScrollPane id="ScrollPane" fitToHeight="true" fitToWidth="true" minWidth="600.0" pannable="false" prefViewportHeight="567.0" prefViewportWidth="766.0" vbarPolicy="AS_NEEDED" VBox.vgrow="ALWAYS">
<ScrollPane id="ScrollPane" fitToHeight="true" fitToWidth="true" minWidth="600.0" pannable="false"
prefViewportHeight="567.0" prefViewportWidth="766.0" vbarPolicy="AS_NEEDED"
VBox.vgrow="ALWAYS">
<content>
<TilePane fx:id="imageTilePane" hgap="5.0" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefColumns="6" prefHeight="-1.0" prefTileHeight="160.0" prefTileWidth="160.0" prefWidth="-1.0" tileAlignment="TOP_LEFT" vgap="5.0" />
<TilePane fx:id="imageTilePane" hgap="5.0" maxHeight="1.7976931348623157E308"
maxWidth="1.7976931348623157E308" minWidth="-1.0" prefColumns="6" prefHeight="-1.0"
prefTileHeight="160.0" prefTileWidth="160.0" prefWidth="-1.0" tileAlignment="TOP_LEFT"
vgap="5.0"/>
</content>
<VBox.margin>
<Insets />
<Insets/>
</VBox.margin>
</ScrollPane>
</children>
@ -125,13 +154,14 @@
</SplitPane>
</children>
</VBox>
<ToolBar maxHeight="30.0" maxWidth="1.7976931348623157E308" minHeight="30.0" orientation="HORIZONTAL" prefHeight="30.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
<ToolBar maxHeight="30.0" maxWidth="1.7976931348623157E308" minHeight="30.0" orientation="HORIZONTAL"
prefHeight="30.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
<items>
<Label text="Progress:" />
<Separator orientation="VERTICAL" prefHeight="200.0" />
<ProgressBar prefWidth="200.0" progress="0.0" />
<Separator orientation="VERTICAL" prefHeight="200.0" />
<Label text="&lt;PROGRESS INFORMATION&gt;" />
<Label text="Progress:"/>
<Separator orientation="VERTICAL" prefHeight="200.0"/>
<ProgressBar prefWidth="200.0" progress="0.0"/>
<Separator orientation="VERTICAL" prefHeight="200.0"/>
<Label text="&lt;PROGRESS INFORMATION&gt;"/>
</items>
</ToolBar>
</children>

6
gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTile.scala

@ -9,14 +9,14 @@ import com.sothr.imagetools.engine.image.Image
*
* Created by drew on 8/22/14.
*/
class ImageTile extends VBox{
class ImageTile extends VBox {
var imageData: Image = null
def getImageData:Image = {
def getImageData: Image = {
imageData
}
def setImageData(image:Image) = {
def setImageData(image: Image) = {
this.imageData = image
}
}

27
gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTileFactory.scala

@ -7,7 +7,6 @@ import javafx.scene.control.{Label, Tooltip}
import javafx.scene.image.{Image, ImageView}
import javafx.scene.input.MouseEvent
import com.sothr.imagetools.engine.image
import grizzled.slf4j.Logging
import resource._
@ -18,29 +17,29 @@ import resource._
*/
object ImageTileFactory extends Logging {
def get(image:image.Image):ImageTile = {
def get(image: com.sothr.imagetools.engine.image.Image): ImageTile = {
val imageTile = new ImageTile()
imageTile.setImageData(image)
//set tile size
imageTile.setPrefSize(160.0d,160.0d)
imageTile.setMinSize(160.0d,160.0d)
imageTile.setMaxSize(160.0d,160.0d)
imageTile.setPrefSize(160.0d, 160.0d)
imageTile.setMinSize(160.0d, 160.0d)
imageTile.setMaxSize(160.0d, 160.0d)
//set padding
imageTile.setPadding(new Insets(2,2,2,2))
imageTile.setPadding(new Insets(2, 2, 2, 2))
//imageTile.setSpacing(5.0d)
imageTile.setAlignment(Pos.TOP_CENTER)
imageTile.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler[MouseEvent] {
override def handle(event: MouseEvent): Unit = {
if (event.isPrimaryButtonDown) {
//double click
if (event.getClickCount == 2) {
override def handle(event: MouseEvent): Unit = {
if (event.isPrimaryButtonDown) {
//double click
if (event.getClickCount == 2) {
} else {
} else {
}
} else if (event.isSecondaryButtonDown) {
//right click context menu
}
} else if (event.isSecondaryButtonDown) {
//right click context menu
}
}
})

74
gui/src/main/scala/com/sothr/imagetools/ui/controller/AppController.scala

@ -10,9 +10,9 @@ import javafx.scene.web.WebView
import javafx.scene.{Group, Node, Scene}
import javafx.stage.{DirectoryChooser, Stage, StageStyle}
import com.sothr.imagetools.engine.util.{PropertiesService, ResourceLoader}
import com.sothr.imagetools.engine.{ConcurrentEngine, Engine}
import com.sothr.imagetools.ui.component.ImageTileFactory
import com.sothr.imagetools.engine.util.{PropertiesService, ResourceLoader}
import grizzled.slf4j.Logging
import org.markdown4j.Markdown4jProcessor
@ -24,19 +24,19 @@ import org.markdown4j.Markdown4jProcessor
class AppController extends Logging {
//Define controls
@FXML var rootPane : javafx.scene.layout.AnchorPane = null
@FXML var rootMenuBar : javafx.scene.control.MenuBar = null
@FXML var imageTilePane : javafx.scene.layout.TilePane = null
@FXML var tagListView : javafx.scene.control.ListView[String] = null
@FXML var rootPane: javafx.scene.layout.AnchorPane = null
@FXML var rootMenuBar: javafx.scene.control.MenuBar = null
@FXML var imageTilePane: javafx.scene.layout.TilePane = null
@FXML var tagListView: javafx.scene.control.ListView[String] = null
// Labels
@FXML var selectedDirectoryLabel: javafx.scene.control.Label = null
// Engine
val engine:Engine = new ConcurrentEngine()
val engine: Engine = new ConcurrentEngine()
// Current State
var currentDirectory:String = "."
var currentDirectory: String = "."
@FXML def initialize() = {
if (PropertiesService.has("lastPath")) {
@ -61,12 +61,12 @@ class AppController extends Logging {
//region MenuItem Actions
@FXML
def helpAction(event:ActionEvent) = {
def helpAction(event: ActionEvent) = {
showExternalHTMLUtilityDialog("http://www.sothr.com")
}
@FXML
def aboutAction(event:ActionEvent) = {
def aboutAction(event: ActionEvent) = {
debug("Displaying about screen")
var aboutMessage = "Simple About Message"
try {
@ -80,7 +80,7 @@ class AppController extends Logging {
debug(s"Parsed About Message: '$aboutMessage'")
} catch {
case ioe:IOException =>
case ioe: IOException =>
error("Unable to read about file")
}
@ -89,14 +89,14 @@ class AppController extends Logging {
}
@FXML
def closeAction(event:ActionEvent) = {
def closeAction(event: ActionEvent) = {
debug("Closing application from the menu bar")
val stage:Stage = this.rootMenuBar.getScene.getWindow.asInstanceOf[Stage]
val stage: Stage = this.rootMenuBar.getScene.getWindow.asInstanceOf[Stage]
stage.close()
}
@FXML
def browseFolders(event:ActionEvent) = {
def browseFolders(event: ActionEvent) = {
val chooser = new DirectoryChooser()
chooser.setTitle("ImageTools Browser")
@ -108,11 +108,11 @@ class AppController extends Logging {
selectedDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
currentDirectory = selectedDirectory.getAbsolutePath
PropertiesService.set("lastPath",selectedDirectory.getAbsolutePath)
PropertiesService.set("lastPath", selectedDirectory.getAbsolutePath)
}
@FXML
def showAllImages(event:ActionEvent) = {
def showAllImages(event: ActionEvent) = {
imageTilePane.getChildren.setAll(new util.ArrayList[Node]())
val images = engine.getImagesForDirectory(currentDirectory)
info(s"Displaying ${images.length} images")
@ -123,14 +123,14 @@ class AppController extends Logging {
}
@FXML
def showSimilarImages(event:ActionEvent) = {
def showSimilarImages(event: ActionEvent) = {
imageTilePane.getChildren.setAll(new util.ArrayList[Node]())
val similarImages = engine.getSimilarImagesForDirectory(currentDirectory)
info(s"Displaying ${similarImages.length} similar images")
for (similarImage <- similarImages) {
debug(s"Adding similar images ${similarImage.rootImage.toString} to app")
imageTilePane.getChildren.add(ImageTileFactory.get(similarImage.rootImage))
similarImage.similarImages.foreach( image => imageTilePane.getChildren.add(ImageTileFactory.get(image)))
similarImage.similarImages.foreach(image => imageTilePane.getChildren.add(ImageTileFactory.get(image)))
}
}
@ -139,7 +139,7 @@ class AppController extends Logging {
//todo: include a templating engine for rendering information
//todo: show a dialog that is rendered from markdown content
def showMarkdownUtilityDialog(title:String, markdown:String, width:Double = 800.0, height:Double = 600.0) = {
def showMarkdownUtilityDialog(title: String, markdown: String, width: Double = 800.0, height: Double = 600.0) = {
val htmlBody = new Markdown4jProcessor().process(markdown)
showHTMLUtilityDialog(title, htmlBody, width, height)
}
@ -152,10 +152,10 @@ class AppController extends Logging {
* @param width Desired width of the dialog
* @param height Desired height of the dialog
*/
def showHTMLUtilityDialog(title:String, htmlBody:String, width:Double = 800.0, height:Double = 600.0) = {
val dialog:Stage = new Stage()
def showHTMLUtilityDialog(title: String, htmlBody: String, width: Double = 800.0, height: Double = 600.0) = {
val dialog: Stage = new Stage()
dialog.initStyle(StageStyle.UTILITY)
val parent:Group = new Group()
val parent: Group = new Group()
//setup the HTML view
val htmlView = new WebView
@ -166,17 +166,17 @@ class AppController extends Logging {
htmlView.setPrefHeight(height)
parent.getChildren.add(htmlView)
val scene:Scene = new Scene(parent)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setTitle(title)
dialog.show()
}
def showExternalHTMLUtilityDialog(url:String) = {
val dialog:Stage = new Stage()
def showExternalHTMLUtilityDialog(url: String) = {
val dialog: Stage = new Stage()
dialog.initStyle(StageStyle.UTILITY)
val parent:Group = new Group()
val parent: Group = new Group()
//setup the HTML view
val htmlView = new WebView
@ -187,7 +187,7 @@ class AppController extends Logging {
//htmlView.setPrefHeight(height)
parent.getChildren.add(htmlView)
val scene:Scene = new Scene(parent)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setTitle(htmlView.getEngine.getTitle)
@ -201,15 +201,15 @@ class AppController extends Logging {
* @param wrapWidth When to wrap
* @param alignment How it should be aligned
*/
def showUtilityDialog(title:String,
message:String,
wrapWidth:Double=300.0,
xOffset:Double = 25.0,
yOffset:Double = 25.0,
alignment:TextAlignment=TextAlignment.JUSTIFY) = {
val dialog:Stage = new Stage()
def showUtilityDialog(title: String,
message: String,
wrapWidth: Double = 300.0,
xOffset: Double = 25.0,
yOffset: Double = 25.0,
alignment: TextAlignment = TextAlignment.JUSTIFY) = {
val dialog: Stage = new Stage()
dialog.initStyle(StageStyle.UTILITY)
val parent:Group = new Group()
val parent: Group = new Group()
// fill the text box
val messageText = new Text()
@ -220,15 +220,15 @@ class AppController extends Logging {
messageText.setTextAlignment(TextAlignment.JUSTIFY)
parent.getChildren.add(messageText)
val scene:Scene = new Scene(parent)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setMinWidth(wrapWidth+xOffset*2)
dialog.setMinWidth(wrapWidth + xOffset * 2)
dialog.setTitle(title)
dialog.show()
}
def print():String = {
def print(): String = {
"This method works"
}
}

2
gui/version.info

@ -1 +1 @@
0.1.1-DEV-13-661ba6d
0.1.1-DEV-15-15ce4f1

46
parent/pom.xml

@ -1,7 +1,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sothr.imagetools</groupId>
<artifactId>Parent</artifactId>
<version>1.0.0</version>
@ -10,13 +10,13 @@
<name>Image-Tools-Parent</name>
<pluginRepositories>
<pluginRepository>
<id>sonatype-releases</id>
<url>http://oss.sonatype.org/content/repositories/releases</url>
</pluginRepository>
<pluginRepository>
<id>clojars.org</id>
<url>http://clojars.org/repo</url>
<pluginRepository>
<id>sonatype-releases</id>
<url>http://oss.sonatype.org/content/repositories/releases</url>
</pluginRepository>
<pluginRepository>
<id>clojars.org</id>
<url>http://clojars.org/repo</url>
</pluginRepository>
</pluginRepositories>
@ -66,19 +66,19 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${lib.logback.version}</version>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${lib.logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${lib.logback.version}</version>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${lib.logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>${lib.logback.version}</version>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>${lib.logback.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
@ -156,9 +156,9 @@
<version>${lib.hibernate.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>${lib.hibernate.ehcache.version}</version>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>${lib.hibernate.ehcache.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
@ -263,8 +263,8 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<version>3.1</version>
<configuration>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
</configuration>

2
pom.xml

@ -1,5 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sothr.imagetools</groupId>

Loading…
Cancel
Save