From d474b42952c1529604e9e146befea7b5286932f1 Mon Sep 17 00:00:00 2001 From: Drew Short Date: Mon, 3 Feb 2014 15:45:48 -0500 Subject: [PATCH] Massive improvements in the concurrent engine --- pom.xml | 4 +- .../sothr/imagetools/ConcurrentEngine.scala | 134 +++++++++--------- .../sothr/imagetools/dto/ImageHashDTO.scala | 9 +- .../sothr/imagetools/hash/HashService.scala | 78 +++++----- .../com/sothr/imagetools/image/Image.scala | 13 ++ .../imagetools/image/SimilarImages.scala | 9 ++ .../imagetools/util/PropertiesService.scala | 32 +++++ 7 files changed, 169 insertions(+), 110 deletions(-) diff --git a/pom.xml b/pom.xml index ed6b48b..37c967a 100644 --- a/pom.xml +++ b/pom.xml @@ -328,7 +328,9 @@ - + + + diff --git a/src/main/scala/com/sothr/imagetools/ConcurrentEngine.scala b/src/main/scala/com/sothr/imagetools/ConcurrentEngine.scala index 91d5e0c..11fcec4 100644 --- a/src/main/scala/com/sothr/imagetools/ConcurrentEngine.scala +++ b/src/main/scala/com/sothr/imagetools/ConcurrentEngine.scala @@ -2,7 +2,7 @@ package com.sothr.imagetools import java.io.File import akka.actor.{Actor, ActorSystem, Props, ActorLogging} -import akka.routing.{Broadcast, SmallestMailboxRouter} +import akka.routing.{Broadcast, RoundRobinRouter, SmallestMailboxRouter} import akka.pattern.ask import akka.util.Timeout import java.util.concurrent.TimeUnit @@ -55,55 +55,15 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging { images.toList } - //copied from the sequential engine as the concurrent version has issues currently - def getSimilarImagesForDirectory(directoryPath:String):List[SimilarImages] = { - debug(s"Looking for similar images in directory: $directoryPath") - val images = getImagesForDirectory(directoryPath) - info(s"Searching ${images.length} images for similarities") - val ignoreSet = new mutable.HashSet[Image]() - val allSimilarImages = new mutable.MutableList[SimilarImages]() - var processedCount = 0 - var similarCount = 0 - 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") - } - debug(s"Looking for images similar to: ${rootImage.imagePath}") - ignoreSet += rootImage - val similarImages = new mutable.MutableList[Image]() - for (image <- images) { - if (!ignoreSet.contains(image)) { - if (rootImage.isSimilarTo(image)) { - debug(s"Image: ${image.imagePath} is similar") - similarImages += image - ignoreSet += image - similarCount += 1 - } - } - } - if (similarImages.length > 1) { - val similar = new SimilarImages(rootImage, similarImages.toList) - debug(s"Found similar images: ${similar.toString}") - allSimilarImages += similar - } - processedCount += 1 - } - } - info(s"Finished processing ${images.size} images. Found $similarCount similar images") - allSimilarImages.toList - } - - /* //needs to be rebuilt def getSimilarImagesForDirectory(directoryPath:String):List[SimilarImages] = { debug(s"Looking for similar images in directory: $directoryPath") val images = getImagesForDirectory(directoryPath) + engineSimilarityController ! EngineCompareSetImages(images) info(s"Searching ${images.length} images for similarities") - val allSimilarImages = new mutable.MutableList[SimilarImages]() for (rootImage <- images) { debug(s"Looking for images similar to: ${rootImage.imagePath}") - engineSimilarityController ! EngineCompareImages(rootImage, images, null) + engineSimilarityController ! EngineCompareImages(rootImage) } //tell the comparison engine there's nothing left to compare engineSimilarityController ! EngineNoMoreComparisons @@ -124,16 +84,30 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging { } val f = engineSimilarityController ? EngineGetSimilarityResults val result = Await.result(f, timeout.duration).asInstanceOf[List[SimilarImages]] - allSimilarImages ++= result - + //process the result into a list we want in cleanedSimilarImages + var count = 0 + 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 + } + } + } + var similarCount = 0 - for (similarImage <- allSimilarImages) { + for (similarImage <- cleanedSimilarImages) { similarCount += 1 + similarImage.similarImages.size } info(s"Finished processing ${images.size} images. Found $similarCount similar images") - allSimilarImages.toList - }*/ + cleanedSimilarImages.toList + } } @@ -166,6 +140,11 @@ class ConcurrentEngineProcessingController extends Actor with ActorLogging { var processorsFinished = 0 + override def preStart() = { + log.info("Staring the controller for processing images") + log.info("Using {} actors to process images", numOfRouters) + } + override def receive = { case command:EngineProcessFile => processFile(command) case command:EngineFileProcessed => fileProcessed(command) @@ -189,7 +168,7 @@ class ConcurrentEngineProcessingController extends Actor with ActorLogging { def fileProcessed(command:EngineFileProcessed) = { processed += 1 - if (processed % 25 == 0) log.info(s"Processed $processed/$toProcess") + if (processed % 25 == 0 || processed == toProcess) log.info(s"Processed $processed/$toProcess") if (command.image != null) { log.debug(s"processed image: ${command.image.imagePath}") images += command.image @@ -267,15 +246,15 @@ class ConcurrentEngineProcessingActor extends Actor with ActorLogging { } //finding similarities between images -case class EngineCompareImages(image1:Image,images:List[Image],ignoreList:Set[Image]) +case class EngineCompareImages(image1:Image) case class EngineCompareImagesComplete(similarImages:SimilarImages) +case class EngineCompareSetImages(images:List[Image]) case object EngineNoMoreComparisons case object EngineIsSimilarityFinished case object EngineGetSimilarityResults case object EngineActorCompareImagesFinished class ConcurrentEngineSimilarityController extends Actor with ActorLogging { - val imageCache = AppConfig.cacheManager.getCache("images") val numOfRouters = { val max = PropertiesService.get(PropertiesEnum.ConcurrentSimiliartyLimit.toString).toInt val processors = Runtime.getRuntime.availableProcessors() @@ -283,18 +262,24 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging { if (processors > max) threads = max else if (processors > 1) threads = processors - 1 else threads = 1 threads } - val router = context.actorOf(Props[ConcurrentEngineSimilarityActor].withRouter(SmallestMailboxRouter(nrOfInstances = numOfRouters))) + val router = context.actorOf(Props[ConcurrentEngineSimilarityActor].withRouter(RoundRobinRouter(nrOfInstances = numOfRouters))) val allSimilarImages = new mutable.MutableList[SimilarImages] - val ignoreList = new mutable.HashSet[Image]() + var numImages = 0 var toProcess = 0 var processed = 0 var processorsFinished = 0 + override def preStart() = { + log.info("Staring the controller for processing similarites between images") + log.info("Using {} actors to process image similarites", numOfRouters) + } + override def receive = { case command:EngineCompareImages => findSimilarities(command) case command:EngineCompareImagesComplete => similarityProcessed(command) + case command:EngineCompareSetImages => setImageList(command) case EngineNoMoreComparisons => requestWrapup() case EngineActorCompareImagesFinished => actorProcessingFinished() case EngineIsSimilarityFinished => isProcessingFinished() @@ -302,24 +287,27 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging { case _ => log.info("received unknown message") } + def setImageList(command:EngineCompareSetImages) = { + numImages = command.images.length + router ! Broadcast(command) + } + def findSimilarities(command:EngineCompareImages) = { - log.debug(s"Finding similarities between ${command.image1.imagePath} and ${command.images.length} images") + log.debug(s"Finding similarities between ${command.image1.imagePath} and $numImages images") toProcess += 1 + if (toProcess % 250 == 0 || toProcess == numImages) { + log.info("Sent {}/{} images to be processed for similarites", toProcess, numImages) + } //just relay the command to our workers - router ! EngineCompareImages(command.image1, command.images, ignoreList.toSet[Image]) + router ! EngineCompareImages(command.image1) } def similarityProcessed(command:EngineCompareImagesComplete) = { processed += 1 - if (processed % 25 == 0) log.info(s"Processed $processed/$toProcess") + if (processed % 25 == 0 || processed == numImages) log.info(s"Processed $processed/$toProcess") if (command.similarImages != null) { - if (!ignoreList.contains(command.similarImages.rootImage)) { - log.debug(s"Found similar images: ${command.similarImages}") - allSimilarImages += command.similarImages - //add the similar images to the ignore list so we don't re-process them constantly - ignoreList += command.similarImages.rootImage - ignoreList ++= command.similarImages.similarImages - } + log.debug(s"Found similar images: ${command.similarImages}") + allSimilarImages += command.similarImages } } @@ -332,6 +320,7 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging { */ def actorProcessingFinished() = { processorsFinished += 1 + log.debug("Similarity Processor Reported Finished") } /* @@ -339,6 +328,7 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging { */ def isProcessingFinished() = { try { + log.debug("Processors Finished {}/{}", processorsFinished, numOfRouters) if (processorsFinished >= numOfRouters) sender ! true else sender ! false } catch { case e: Exception ⇒ @@ -367,21 +357,30 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging { class ConcurrentEngineSimilarityActor extends Actor with ActorLogging { var ignoreMessages = false + var imageList = new mutable.MutableList[Image]() override def receive = { case command:EngineCompareImages => compareImages(command) + case command:EngineCompareSetImages => cloneAndSetImages(command) case EngineNoMoreComparisons => finishedComparisons() case EngineActorReactivate => ignoreMessages = false case _ => log.info("received unknown message") } + def cloneAndSetImages(command:EngineCompareSetImages) = { + imageList.clear() + for (image <- command.images) { + imageList += image.cloneImage + } + log.debug("Added {} cloned images to internal list", imageList.length) + } + def compareImages(command:EngineCompareImages) = { if (!ignoreMessages) { val similarImages = new mutable.MutableList[Image]() - for (image <- command.images) { - if (!command.ignoreList.contains(image) && command.image1 != image) { + for (image <- imageList) { + if (!command.image1.equals(image)) { if (HashService.areImageHashesSimilar(command.image1.hashes, image.hashes)) { similarImages += image - var ignoreMessages = false } } } @@ -399,7 +398,10 @@ class ConcurrentEngineSimilarityActor extends Actor with ActorLogging { def finishedComparisons() = { if (!ignoreMessages) { + log.info("Commanded to finish processing") ignoreMessages = true + imageList.clear() + log.debug("Finished processing comparisons") sender ! EngineActorCompareImagesFinished } } diff --git a/src/main/scala/com/sothr/imagetools/dto/ImageHashDTO.scala b/src/main/scala/com/sothr/imagetools/dto/ImageHashDTO.scala index 24985f1..1849a96 100644 --- a/src/main/scala/com/sothr/imagetools/dto/ImageHashDTO.scala +++ b/src/main/scala/com/sothr/imagetools/dto/ImageHashDTO.scala @@ -4,6 +4,10 @@ import grizzled.slf4j.Logging class ImageHashDTO(val ahash:Long, val dhash:Long, val phash:Long, val md5:String) extends Serializable with Logging{ + def cloneHashes:ImageHashDTO = { + return new ImageHashDTO(ahash,dhash,phash,md5) + } + override def hashCode():Int = { var result = 365 result = 41 * result + (this.ahash ^ (this.ahash >>> 32)).toInt @@ -13,9 +17,6 @@ class ImageHashDTO(val ahash:Long, val dhash:Long, val phash:Long, val md5:Strin } override def toString:String = { - s"""MD5: $md5 - ahash: $ahash - dhash: $dhash - phash: $phash""".stripMargin + s"MD5: $md5 ahash: $ahash dhash: $dhash phash: $phash" } } diff --git a/src/main/scala/com/sothr/imagetools/hash/HashService.scala b/src/main/scala/com/sothr/imagetools/hash/HashService.scala index b899970..d528551 100644 --- a/src/main/scala/com/sothr/imagetools/hash/HashService.scala +++ b/src/main/scala/com/sothr/imagetools/hash/HashService.scala @@ -23,7 +23,7 @@ object HashService extends Logging { } def getImageHashes(image:BufferedImage, imagePath:String):ImageHashDTO = { - debug(s"Creating hashes for image") + debug("Creating hashes for an image") var ahash:Long = 0L var dhash:Long = 0L @@ -33,13 +33,13 @@ object HashService extends Logging { //Get Image Data val grayImage = ImageService.convertToGray(image) - if (PropertiesService.get(PropertiesEnum.UseAhash.toString) == "true") { + if (PropertiesService.useAhash == true) { ahash = getAhash(grayImage, true) } - if (PropertiesService.get(PropertiesEnum.UseAhash.toString) == "true") { + if (PropertiesService.useDhash == true) { dhash = getDhash(grayImage, true) } - if (PropertiesService.get(PropertiesEnum.UseAhash.toString) == "true") { + if (PropertiesService.usePhash == true) { phash = getPhash(grayImage, true) } @@ -57,7 +57,7 @@ object HashService extends Logging { } else { grayImage = ImageService.convertToGray(image) } - val resizedImage = ImageService.resize(grayImage, PropertiesService.get(PropertiesEnum.AhashPrecision.toString).toInt, true) + val resizedImage = ImageService.resize(grayImage, PropertiesService.aHashPrecision, true) val imageData = ImageService.getImageData(resizedImage) AHash.getHash(imageData) } @@ -70,7 +70,7 @@ object HashService extends Logging { } else { grayImage = ImageService.convertToGray(image) } - val resizedImage = ImageService.resize(grayImage, PropertiesService.get(PropertiesEnum.DhashPrecision.toString).toInt, true) + val resizedImage = ImageService.resize(grayImage, PropertiesService.dHashPrecision, true) val imageData = ImageService.getImageData(resizedImage) DHash.getHash(imageData) } @@ -83,7 +83,7 @@ object HashService extends Logging { } else { grayImage = ImageService.convertToGray(image) } - val resizedImage = ImageService.resize(grayImage, PropertiesService.get(PropertiesEnum.PhashPrecision.toString).toInt, true) + val resizedImage = ImageService.resize(grayImage, PropertiesService.pHashPrecision, true) val imageData = ImageService.getImageData(resizedImage) PHash.getHash(imageData) } @@ -93,39 +93,39 @@ object HashService extends Logging { } def areAhashSimilar(ahash1:Long, ahash2:Long):Boolean = { - val tolerence = PropertiesService.get(PropertiesEnum.AhashTolerance.toString).toInt + val tolerence = PropertiesService.aHashTolerance val distance = Hamming.getDistance(ahash1, ahash2) - debug(s"hash1: $ahash1 hash2: $ahash2 tolerence: $tolerence hamming distance: $distance") + //debug(s"hash1: $ahash1 hash2: $ahash2 tolerence: $tolerence hamming distance: $distance") if (distance <= tolerence) true else false } def areDhashSimilar(dhash1:Long, dhash2:Long):Boolean = { - val tolerence = PropertiesService.get(PropertiesEnum.DhashTolerance.toString).toInt + val tolerence = PropertiesService.dHashTolerance val distance = Hamming.getDistance(dhash1, dhash2) - debug(s"hash1: $dhash1 hash2: $dhash2 tolerence: $tolerence hamming distance: $distance") + //debug(s"hash1: $dhash1 hash2: $dhash2 tolerence: $tolerence hamming distance: $distance") if (distance <= tolerence) true else false } def arePhashSimilar(phash1:Long, phash2:Long):Boolean = { - val tolerence = PropertiesService.get(PropertiesEnum.PhashTolerance.toString).toInt + val tolerence = PropertiesService.pHashTolerance val distance = Hamming.getDistance(phash1, phash2) - debug(s"hash1: $phash1 hash2: $phash2 tolerence: $tolerence hamming distance: $distance") + //debug(s"hash1: $phash1 hash2: $phash2 tolerence: $tolerence hamming distance: $distance") if (distance <= tolerence) true else false } def getWeightedHashSimilarity(imageHash1:ImageHashDTO, imageHash2:ImageHashDTO):Float = { //ahash - val aHashTolerance = PropertiesService.get(PropertiesEnum.AhashTolerance.toString).toInt - val aHashWeight = PropertiesService.get(PropertiesEnum.AhashWeight.toString).toFloat - val useAhash = PropertiesService.get(PropertiesEnum.UseAhash.toString).toBoolean + val aHashTolerance = PropertiesService.aHashTolerance + val aHashWeight = PropertiesService.aHashWeight + val useAhash = PropertiesService.useAhash //dhash - val dHashTolerance = PropertiesService.get(PropertiesEnum.DhashTolerance.toString).toInt - val dHashWeight = PropertiesService.get(PropertiesEnum.DhashWeight.toString).toFloat - val useDhash = PropertiesService.get(PropertiesEnum.UseDhash.toString).toBoolean + val dHashTolerance = PropertiesService.dHashTolerance + val dHashWeight = PropertiesService.dHashWeight + val useDhash = PropertiesService.useAhash //phash - val pHashTolerance = PropertiesService.get(PropertiesEnum.PhashTolerance.toString).toInt - val pHashWeight = PropertiesService.get(PropertiesEnum.PhashWeight.toString).toFloat - val usePhash = PropertiesService.get(PropertiesEnum.UsePhash.toString).toBoolean + val pHashTolerance = PropertiesService.pHashTolerance + val pHashWeight = PropertiesService.pHashWeight + val usePhash = PropertiesService.useAhash //calculate weighted values var weightedHammingTotal:Float = 0 @@ -135,41 +135,41 @@ object HashService extends Logging { { 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") + //debug(s"hash1: ${imageHash1.ahash} hash2: ${imageHash1.ahash} tolerence: $aHashTolerance hamming distance: $hamming weight: $aHashWeight") methodsTotal+=1 } 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") + //debug(s"hash1: ${imageHash1.dhash} hash2: ${imageHash1.dhash} tolerence: $dHashTolerance hamming distance: $hamming weight: $dHashWeight") methodsTotal+=1 } 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") + //debug(s"hash1: ${imageHash1.phash} hash2: ${imageHash1.phash} tolerence: $pHashTolerance hamming distance: $hamming weight: $pHashWeight") methodsTotal+=1 } val weightedHammingMean = weightedHammingTotal / methodsTotal - debug(s"Calculated Weighted Hamming Mean: $weightedHammingMean") + //debug(s"Calculated Weighted Hamming Mean: $weightedHammingMean") weightedHammingMean } def getWeightedHashTolerence:Float = { //ahash - val aHashTolerance = PropertiesService.get(PropertiesEnum.AhashTolerance.toString).toInt - val aHashWeight = PropertiesService.get(PropertiesEnum.AhashWeight.toString).toFloat - val useAhash = PropertiesService.get(PropertiesEnum.UseAhash.toString).toBoolean + val aHashTolerance = PropertiesService.aHashTolerance + val aHashWeight = PropertiesService.aHashWeight + val useAhash = PropertiesService.useAhash //dhash - val dHashTolerance = PropertiesService.get(PropertiesEnum.DhashTolerance.toString).toInt - val dHashWeight = PropertiesService.get(PropertiesEnum.DhashWeight.toString).toFloat - val useDhash = PropertiesService.get(PropertiesEnum.UseDhash.toString).toBoolean + val dHashTolerance = PropertiesService.dHashTolerance + val dHashWeight = PropertiesService.dHashWeight + val useDhash = PropertiesService.useAhash //phash - val pHashTolerance = PropertiesService.get(PropertiesEnum.PhashTolerance.toString).toInt - val pHashWeight = PropertiesService.get(PropertiesEnum.PhashWeight.toString).toFloat - val usePhash = PropertiesService.get(PropertiesEnum.UsePhash.toString).toBoolean + val pHashTolerance = PropertiesService.pHashTolerance + val pHashWeight = PropertiesService.pHashWeight + val usePhash = PropertiesService.useAhash //calculate weighted values var weightedToleranceTotal:Float = 0 @@ -178,23 +178,23 @@ object HashService extends Logging { if (useAhash) { weightedToleranceTotal += aHashTolerance * aHashWeight - debug(s"Ahash Tolerance: $aHashTolerance Current Weighted Tolerance: $weightedToleranceTotal") + //debug(s"Ahash Tolerance: $aHashTolerance Current Weighted Tolerance: $weightedToleranceTotal") methodsTotal+=1 } if (useDhash) { weightedToleranceTotal += dHashTolerance * dHashWeight - debug(s"Dhash Tolerance: $dHashTolerance Current Weighted Tolerance: $weightedToleranceTotal") + //debug(s"Dhash Tolerance: $dHashTolerance Current Weighted Tolerance: $weightedToleranceTotal") methodsTotal+=1 } if (usePhash) { weightedToleranceTotal += pHashTolerance * pHashWeight - debug(s"Phash Tolerance: $pHashTolerance Current Weighted Tolerance: $weightedToleranceTotal") + //debug(s"Phash Tolerance: $pHashTolerance Current Weighted Tolerance: $weightedToleranceTotal") methodsTotal+=1 } val weightedTolerance = weightedToleranceTotal / methodsTotal - debug(s"Calculated Weighted Tolerance: $weightedTolerance") + //debug(s"Calculated Weighted Tolerance: $weightedTolerance") weightedTolerance } diff --git a/src/main/scala/com/sothr/imagetools/image/Image.scala b/src/main/scala/com/sothr/imagetools/image/Image.scala index 4d15e16..3c1e3fb 100644 --- a/src/main/scala/com/sothr/imagetools/image/Image.scala +++ b/src/main/scala/com/sothr/imagetools/image/Image.scala @@ -9,6 +9,7 @@ class Image(val imagePath:String, val thumbnailPath:String, val imageSize:Tuple2 var imageType:ImageType = ImageType.SingleFrameImage def isSimilarTo(otherImage:Image):Boolean = { + debug("Checking {} for similarities with {}",imagePath, otherImage.imagePath) HashService.areImageHashesSimilar(this.hashes,otherImage.hashes) } @@ -20,10 +21,22 @@ class Image(val imagePath:String, val thumbnailPath:String, val imageSize:Tuple2 }*/ + def cloneImage:Image = { + return new Image(imagePath,thumbnailPath,imageSize,hashes.cloneHashes) + } + 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 hashCode:Int = { var result = 365 result = 37 * result + imagePath.hashCode diff --git a/src/main/scala/com/sothr/imagetools/image/SimilarImages.scala b/src/main/scala/com/sothr/imagetools/image/SimilarImages.scala index 75c0d3c..09878fa 100644 --- a/src/main/scala/com/sothr/imagetools/image/SimilarImages.scala +++ b/src/main/scala/com/sothr/imagetools/image/SimilarImages.scala @@ -15,6 +15,15 @@ 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 toString:String = { s"""RootImage: ${rootImage.imagePath} diff --git a/src/main/scala/com/sothr/imagetools/util/PropertiesService.scala b/src/main/scala/com/sothr/imagetools/util/PropertiesService.scala index 0f7eb58..103f553 100644 --- a/src/main/scala/com/sothr/imagetools/util/PropertiesService.scala +++ b/src/main/scala/com/sothr/imagetools/util/PropertiesService.scala @@ -41,6 +41,22 @@ object PropertiesService extends Logging { InfoLogEnabled = get(PropertiesEnum.LogInfo.toString).toBoolean ErrorLogEnabled = get(PropertiesEnum.LogError.toString).toBoolean TimingEnabled = get(PropertiesEnum.Timed.toString).toBoolean + + //ahash + aHashPrecision = PropertiesService.get(PropertiesEnum.AhashPrecision.toString).toInt + aHashTolerance = PropertiesService.get(PropertiesEnum.AhashTolerance.toString).toInt + aHashWeight = PropertiesService.get(PropertiesEnum.AhashWeight.toString).toFloat + useAhash = PropertiesService.get(PropertiesEnum.UseAhash.toString).toBoolean + //dhash + dHashPrecision = PropertiesService.get(PropertiesEnum.DhashPrecision.toString).toInt + dHashTolerance = PropertiesService.get(PropertiesEnum.DhashTolerance.toString).toInt + dHashWeight = PropertiesService.get(PropertiesEnum.DhashWeight.toString).toFloat + useDhash = PropertiesService.get(PropertiesEnum.UseDhash.toString).toBoolean + //phash + pHashPrecision = PropertiesService.get(PropertiesEnum.PhashPrecision.toString).toInt + pHashTolerance = PropertiesService.get(PropertiesEnum.PhashTolerance.toString).toInt + pHashWeight = PropertiesService.get(PropertiesEnum.PhashWeight.toString).toFloat + usePhash = PropertiesService.get(PropertiesEnum.UsePhash.toString).toBoolean info("Loaded Special Properties") } @@ -80,5 +96,21 @@ object PropertiesService extends Logging { var InfoLogEnabled:Boolean = false var ErrorLogEnabled:Boolean = false var TimingEnabled:Boolean = false + + //ahash + var aHashPrecision = 0 + var aHashTolerance = 0 + var aHashWeight = 0.0f + var useAhash = false + //dhash + var dHashPrecision = 0 + var dHashTolerance = 0 + var dHashWeight = 0.0f + var useDhash = false + //phash + var pHashPrecision = 0 + var pHashTolerance = 0 + var pHashWeight = 0.0f + var usePhash = false } \ No newline at end of file