Browse Source

Refactor: Cleanup and performance enhancements

* Full imageHash calculation improved between 10-20% with greater performance enhancements for larger images.
* Cleaned up code
* Reduced code duplication
* Adapted engine to hash lib 0.2.0
* Adjusted library versions where appropriate
master
Drew Short 7 years ago
parent
commit
4b31440293
  1. 3
      engine/src/main/scala/com/sothr/imagetools/engine/ConcurrentEngine.scala
  2. 13
      engine/src/main/scala/com/sothr/imagetools/engine/image/ImageService.scala
  3. 29
      engine/src/main/scala/com/sothr/imagetools/engine/util/PropertiesService.scala
  4. 7
      hash/pom.xml
  5. 400
      hash/src/main/scala/com/sothr/imagetools/hash/HashService.scala
  6. 8
      hash/src/main/scala/com/sothr/imagetools/hash/HashSetting.scala
  7. 25
      hash/src/main/scala/com/sothr/imagetools/hash/ImageHash.scala
  8. 8
      hash/src/main/scala/com/sothr/imagetools/hash/dto/HashSettingDTO.scala
  9. 46
      hash/src/main/scala/com/sothr/imagetools/hash/dto/ImageHashDTO.scala
  10. 83
      hash/src/main/scala/com/sothr/imagetools/hash/type/AHash.scala
  11. 96
      hash/src/main/scala/com/sothr/imagetools/hash/type/DHash.scala
  12. 113
      hash/src/main/scala/com/sothr/imagetools/hash/type/PHash.scala
  13. 10
      hash/src/main/scala/com/sothr/imagetools/hash/type/PerceptualHasher.scala
  14. 25
      hash/src/main/scala/com/sothr/imagetools/hash/util/HammingUtil.scala
  15. 201
      hash/src/main/scala/com/sothr/imagetools/hash/util/ImageUtil.scala
  16. 49
      hash/src/main/scala/com/sothr/imagetools/hash/util/TimingUtil.scala
  17. 9
      hash/src/test/scala/com/sothr/imagetools/hash/BaseTest.scala
  18. 93
      hash/src/test/scala/com/sothr/imagetools/hash/HashServiceAHashTest.scala
  19. 21
      hash/src/test/scala/com/sothr/imagetools/hash/HashServiceBaseTest.scala
  20. 110
      hash/src/test/scala/com/sothr/imagetools/hash/HashServiceDHashTest.scala
  21. 67
      hash/src/test/scala/com/sothr/imagetools/hash/HashServiceMD5Test.scala
  22. 93
      hash/src/test/scala/com/sothr/imagetools/hash/HashServicePHashTest.scala
  23. 67
      hash/src/test/scala/com/sothr/imagetools/hash/HashServiceSHA1Test.scala
  24. 578
      hash/src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala
  25. 7
      hash/src/test/scala/com/sothr/imagetools/hash/TestParams.scala
  26. 4
      parent/pom.xml

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

@ -7,10 +7,9 @@ import akka.actor.{Actor, ActorLogging, ActorRef, PoisonPill, Props}
import akka.pattern.ask import akka.pattern.ask
import akka.routing.{Broadcast, RoundRobinPool, SmallestMailboxPool} import akka.routing.{Broadcast, RoundRobinPool, SmallestMailboxPool}
import akka.util.Timeout import akka.util.Timeout
import com.sothr.imagetools.hash.HashService
import com.sothr.imagetools.hash.{HashService, ImageHash}
import com.sothr.imagetools.engine.image.{Image, ImageService, SimilarImages} import com.sothr.imagetools.engine.image.{Image, ImageService, SimilarImages}
import com.sothr.imagetools.engine.util.{PropertiesService, PropertyEnum} import com.sothr.imagetools.engine.util.{PropertiesService, PropertyEnum}
import com.sothr.imagetools.hash.dto.ImageHashDTO
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.Await import scala.concurrent.Await

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

@ -8,8 +8,7 @@ import com.sothr.imagetools.engine.AppConfig
import com.sothr.imagetools.engine.dao.ImageDAO import com.sothr.imagetools.engine.dao.ImageDAO
import com.sothr.imagetools.engine.util.{PropertiesService, PropertyEnum} import com.sothr.imagetools.engine.util.{PropertiesService, PropertyEnum}
import com.sothr.imagetools.engine.vo.ImageHashVO import com.sothr.imagetools.engine.vo.ImageHashVO
import com.sothr.imagetools.hash.HashService
import com.sothr.imagetools.hash.dto.ImageHashDTO
import com.sothr.imagetools.hash.{HashService, ImageHash}
import com.sothr.imagetools.hash.util.ImageUtil import com.sothr.imagetools.hash.util.ImageUtil
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import net.sf.ehcache.Element import net.sf.ehcache.Element
@ -36,8 +35,8 @@ object ImageService extends Logging {
bufferedImage, bufferedImage,
file.getAbsolutePath) file.getAbsolutePath)
var thumbnailPath = lookupThumbnailPath(hashes.getFileHash)
if (thumbnailPath == null) thumbnailPath = getThumbnail(bufferedImage, hashes.getFileHash)
var thumbnailPath = lookupThumbnailPath(hashes.fileHash)
if (thumbnailPath == null) thumbnailPath = getThumbnail(bufferedImage, hashes.fileHash)
val imageSize = { val imageSize = {
(bufferedImage.getWidth, bufferedImage.getHeight) (bufferedImage.getWidth, bufferedImage.getHeight)
} }
@ -52,11 +51,11 @@ object ImageService extends Logging {
null null
} }
def convertToImageHashDTO(imageHashVO: ImageHashVO): ImageHashDTO = {
new ImageHashDTO(imageHashVO.ahash, imageHashVO.dhash, imageHashVO.phash, imageHashVO.fileHash)
def convertToImageHashDTO(imageHashVO: ImageHashVO): ImageHash = {
new ImageHash(imageHashVO.ahash, imageHashVO.dhash, imageHashVO.phash, imageHashVO.fileHash)
} }
def convertToImageHashVO(imageHashDTO: ImageHashDTO): ImageHashVO = {
def convertToImageHashVO(imageHashDTO: ImageHash): ImageHashVO = {
new ImageHashVO(imageHashDTO.ahash, imageHashDTO.dhash, imageHashDTO.phash, imageHashDTO.fileHash) new ImageHashVO(imageHashDTO.ahash, imageHashDTO.dhash, imageHashDTO.phash, imageHashDTO.fileHash)
} }

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

@ -3,7 +3,7 @@ package com.sothr.imagetools.engine.util
import java.io.{File, FileOutputStream, PrintStream} import java.io.{File, FileOutputStream, PrintStream}
import java.util.Properties import java.util.Properties
import com.sothr.imagetools.hash.dto.HashSettingDTO
import com.sothr.imagetools.hash.HashSetting
import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions} import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
@ -13,19 +13,19 @@ import grizzled.slf4j.Logging
object PropertiesService extends Logging { object PropertiesService extends Logging {
//OS information //OS information
val OS = System.getProperty("os.name", "UNKNOWN")
val OS_VERSION = System.getProperty("os.version", "UNKNOWN")
val OS_ARCH = System.getProperty("os.arch", "UNKNOWN")
val OS: String = System.getProperty("os.name", "UNKNOWN")
val OS_VERSION: String = System.getProperty("os.version", "UNKNOWN")
val OS_ARCH: String = System.getProperty("os.arch", "UNKNOWN")
private val newUserConf: Properties = new Properties() private val newUserConf: Properties = new Properties()
private val configRenderOptions = ConfigRenderOptions.concise().setFormatted(true) private val configRenderOptions = ConfigRenderOptions.concise().setFormatted(true)
//specific highly used properties //specific highly used properties
var TimingEnabled: Boolean = false var TimingEnabled: Boolean = false
var aHashSettings = new HashSettingDTO(false, 0, 0, 0.0f)
var dHashSettings = new HashSettingDTO(false, 0, 0, 0.0f)
var pHashSettings = new HashSettingDTO(false, 0, 0, 0.0f)
private var defaultConf: Config = null
private var userConf: Config = null
private var version: Version = null
var aHashSettings = new HashSetting("AHash", false, 0, 0, 0.0f)
var dHashSettings = new HashSetting("DHash", false, 0, 0, 0.0f)
var pHashSettings = new HashSetting("PHash", false, 0, 0, 0.0f)
private var defaultConf: Config = _
private var userConf: Config = _
private var version: Version = _
def getVersion: Version = this.version def getVersion: Version = this.version
@ -47,21 +47,24 @@ object PropertiesService extends Logging {
//load special properties //load special properties
TimingEnabled = get(PropertyEnum.Timed.toString).toBoolean TimingEnabled = get(PropertyEnum.Timed.toString).toBoolean
aHashSettings = new HashSettingDTO(
aHashSettings = new HashSetting(
"AHash",
get(PropertyEnum.UseAhash.toString).toBoolean, get(PropertyEnum.UseAhash.toString).toBoolean,
get(PropertyEnum.AhashPrecision.toString).toInt, get(PropertyEnum.AhashPrecision.toString).toInt,
get(PropertyEnum.AhashTolerance.toString).toInt, get(PropertyEnum.AhashTolerance.toString).toInt,
get(PropertyEnum.AhashWeight.toString).toFloat get(PropertyEnum.AhashWeight.toString).toFloat
) )
dHashSettings = new HashSettingDTO(
dHashSettings = new HashSetting(
"DHash",
get(PropertyEnum.UseDhash.toString).toBoolean, get(PropertyEnum.UseDhash.toString).toBoolean,
get(PropertyEnum.DhashPrecision.toString).toInt, get(PropertyEnum.DhashPrecision.toString).toInt,
get(PropertyEnum.DhashTolerance.toString).toInt, get(PropertyEnum.DhashTolerance.toString).toInt,
get(PropertyEnum.DhashWeight.toString).toFloat get(PropertyEnum.DhashWeight.toString).toFloat
) )
pHashSettings = new HashSettingDTO(
pHashSettings = new HashSetting(
"PHash",
get(PropertyEnum.UsePhash.toString).toBoolean, get(PropertyEnum.UsePhash.toString).toBoolean,
get(PropertyEnum.PhashPrecision.toString).toInt, get(PropertyEnum.PhashPrecision.toString).toInt,
get(PropertyEnum.PhashTolerance.toString).toInt, get(PropertyEnum.PhashTolerance.toString).toInt,

7
hash/pom.xml

@ -12,7 +12,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>hash</artifactId> <artifactId>hash</artifactId>
<version>0.1.1</version>
<version>0.2.0</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>ImageTools-Hash</name> <name>ImageTools-Hash</name>
@ -57,6 +57,11 @@
<artifactId>scalatest_${scala.binary.version}</artifactId> <artifactId>scalatest_${scala.binary.version}</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

400
hash/src/main/scala/com/sothr/imagetools/hash/HashService.scala

@ -5,7 +5,6 @@ import java.io.{File, FileInputStream}
import javax.imageio.ImageIO import javax.imageio.ImageIO
import com.sothr.imagetools.hash.`type`.{AHash, DHash, PHash} import com.sothr.imagetools.hash.`type`.{AHash, DHash, PHash}
import com.sothr.imagetools.hash.dto.{HashSettingDTO, ImageHashDTO}
import com.sothr.imagetools.hash.util.{HammingUtil, ImageUtil} import com.sothr.imagetools.hash.util.{HammingUtil, ImageUtil}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.codec.digest.DigestUtils
@ -18,169 +17,240 @@ import resource.managed
*/ */
object HashService extends Logging { object HashService extends Logging {
def getImageHashes(ahashSettings: HashSettingDTO,
dhashSettings: HashSettingDTO,
phashSettings: HashSettingDTO,
imagePath: String): ImageHashDTO = {
getImageHashes(ahashSettings, dhashSettings, phashSettings, ImageIO.read(new File(imagePath)), imagePath)
}
def getImageHashes(ahashSettings: HashSettingDTO,
dhashSettings: HashSettingDTO,
phashSettings: HashSettingDTO,
image: BufferedImage,
imagePath: String): ImageHashDTO = {
debug(s"Creating hashes for $imagePath")
var ahash: Long = 0L
var dhash: Long = 0L
var phash: Long = 0L
val sha1: String = getSHA1(imagePath)
//Get Image Data
val grayImage = ImageUtil.convertToGray(image)
if (ahashSettings.use) {
ahash = getAhash(ahashSettings, grayImage, alreadyGray = true)
}
if (dhashSettings.use) {
dhash = getDhash(dhashSettings, grayImage, alreadyGray = true)
}
if (phashSettings.use) {
phash = getPhash(phashSettings, grayImage, alreadyGray = true)
}
val hashes = new ImageHashDTO(ahash, dhash, phash, sha1)
debug(s"Generated hashes: $hashes")
hashes
}
def getAhash(hashSettings: HashSettingDTO, image: BufferedImage, alreadyGray: Boolean = false): Long = {
//debug("Started generating an AHash")
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
grayImage = ImageUtil.convertToGray(image)
}
val resizedImage = ImageUtil.resize(grayImage, hashSettings.precision, forced = true)
val imageData = ImageUtil.getImageData(resizedImage)
AHash.getHash(imageData)
}
def getDhash(hashSettings: HashSettingDTO, image: BufferedImage, alreadyGray: Boolean = false): Long = {
//debug("Started generating an DHash")
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
grayImage = ImageUtil.convertToGray(image)
}
val resizedImage = ImageUtil.resize(grayImage, hashSettings.precision, forced = true)
val imageData = ImageUtil.getImageData(resizedImage)
DHash.getHash(imageData)
}
def getPhash(hashSettings: HashSettingDTO, image: BufferedImage, alreadyGray: Boolean = false): Long = {
//debug("Started generating an PHash")
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
grayImage = ImageUtil.convertToGray(image)
}
val resizedImage = ImageUtil.resize(grayImage, hashSettings.precision, forced = true)
val imageData = ImageUtil.getImageData(resizedImage)
PHash.getHash(imageData)
}
def getSHA1(filePath: String): String = {
managed(new FileInputStream(filePath)) acquireAndGet {
input => DigestUtils.sha1Hex(input)
}
}
def getMD5(filePath: String): String = {
managed(new FileInputStream(filePath)) acquireAndGet {
input => DigestUtils.md5Hex(input)
}
}
def areHashSimilar(hashSettings: HashSettingDTO, hash1: Long, hash2: Long): Boolean = {
val tolerence = hashSettings.tolerance
val distance = HammingUtil.getDistance(hash1, hash2)
if (distance <= tolerence) true else false
}
def areImageHashesSimilar(ahashSettings: HashSettingDTO,
dhashSettings: HashSettingDTO,
phashSettings: HashSettingDTO,
imageHash1: ImageHashDTO,
imageHash2: ImageHashDTO): Boolean = {
val weightedHammingMean = getWeightedHashSimilarity(ahashSettings, dhashSettings, phashSettings, imageHash1, imageHash2)
val weightedToleranceMean = getWeightedHashTolerence(ahashSettings, dhashSettings, phashSettings)
if (weightedHammingMean <= weightedToleranceMean) true else false
}
def getWeightedHashSimilarity(ahashSettings: HashSettingDTO,
dhashSettings: HashSettingDTO,
phashSettings: HashSettingDTO,
imageHash1: ImageHashDTO,
imageHash2: ImageHashDTO): Float = {
//calculate weighted values
var weightedHammingTotal: Float = 0
var methodsTotal = 0
if (ahashSettings.use) {
val hamming = HammingUtil.getDistance(imageHash1.ahash, imageHash2.ahash)
weightedHammingTotal += hamming * ahashSettings.weight
trace(s"hash1: ${imageHash1.ahash} hash2: ${imageHash1.ahash} tolerance: ${ahashSettings.tolerance} hamming distance: $hamming weight: ${ahashSettings.weight}")
methodsTotal += 1
}
if (dhashSettings.use) {
val hamming = HammingUtil.getDistance(imageHash1.dhash, imageHash2.dhash)
weightedHammingTotal += hamming * dhashSettings.weight
trace(s"hash1: ${imageHash1.dhash} hash2: ${imageHash1.dhash} tolerance: ${dhashSettings.tolerance} hamming distance: $hamming weight: ${dhashSettings.weight}")
methodsTotal += 1
}
if (phashSettings.use) {
val hamming = HammingUtil.getDistance(imageHash1.phash, imageHash2.phash)
weightedHammingTotal += hamming * phashSettings.weight
trace(s"hash1: ${imageHash1.phash} hash2: ${imageHash1.phash} tolerance: ${phashSettings.tolerance} hamming distance: $hamming weight: ${phashSettings.weight}")
methodsTotal += 1
}
val weightedHammingMean = weightedHammingTotal / methodsTotal
debug(s"Calculated Weighted HammingUtil Mean: $weightedHammingMean")
weightedHammingMean
}
def getWeightedHashTolerence(ahashSettings: HashSettingDTO,
dhashSettings: HashSettingDTO,
phashSettings: HashSettingDTO): Float = {
//calculate weighted values
var weightedToleranceTotal: Float = 0
var methodsTotal = 0
if (ahashSettings.use) {
weightedToleranceTotal += ahashSettings.tolerance * ahashSettings.weight
trace(s"Ahash Tolerance: ${ahashSettings.tolerance} Current Weighted Tolerance: $weightedToleranceTotal")
methodsTotal += 1
}
if (dhashSettings.use) {
weightedToleranceTotal += dhashSettings.tolerance * dhashSettings.weight
trace(s"Dhash Tolerance: ${dhashSettings.tolerance} Current Weighted Tolerance: $weightedToleranceTotal")
methodsTotal += 1
}
if (phashSettings.use) {
weightedToleranceTotal += phashSettings.tolerance * phashSettings.weight
trace(s"Phash Tolerance: ${phashSettings.tolerance} Current Weighted Tolerance: $weightedToleranceTotal")
methodsTotal += 1
}
val weightedTolerance = weightedToleranceTotal / methodsTotal
debug(s"Calculated Weighted Tolerance: $weightedTolerance")
weightedTolerance
}
/**
* Given hash settings and an image path, calculate the perceptual hashes
*
* @param ahashSettings { @see com.sothr.imagetools.hash.HashSetting}
* @param dhashSettings { @see com.sothr.imagetools.hash.HashSetting}
* @param phashSettings { @see com.sothr.imagetools.hash.HashSetting}
* @param imagePath Absolute path to the image file
* @return { @see com.sothr.imagetools.hash.ImageHash}
*/
def getImageHashes(ahashSettings: HashSetting,
dhashSettings: HashSetting,
phashSettings: HashSetting,
imagePath: String): ImageHash = {
getImageHashes(ahashSettings,
dhashSettings,
phashSettings,
ImageIO.read(new File(imagePath)),
imagePath)
}
def getPrecisionMap(precisionSet: Set[Int], grayImage: BufferedImage): Map[Int, Array[Array[Int]]] = {
precisionSet.map(p => p -> getImageData(p, grayImage, alreadyGray = true))(collection.breakOut)
}
/**
* Given hash settings, a buffered image and an image path, calculate the perceptual hashes
*
* @param ahashSettings { @see com.sothr.imagetools.hash.HashSetting}
* @param dhashSettings { @see com.sothr.imagetools.hash.HashSetting}
* @param phashSettings { @see com.sothr.imagetools.hash.HashSetting}
* @param image { @see java.awt.image.BufferedImage}
* @param imagePath Absolute path to the image file
* @return { @see com.sothr.imagetools.hash.ImageHash}
*
*/
def getImageHashes(ahashSettings: HashSetting,
dhashSettings: HashSetting,
phashSettings: HashSetting,
image: BufferedImage,
imagePath: String): ImageHash = {
debug(s"Creating hashes for $imagePath")
//Get Image Data
val grayImage = ImageUtil.convertToGray(image)
val precisionSet: Set[Int] = getPrecisionSet(ahashSettings, dhashSettings, phashSettings)
val precisionMap: Map[Int, Array[Array[Int]]] = getPrecisionMap(precisionSet, grayImage)
val ahash: Long = if (ahashSettings.use && precisionMap.contains(ahashSettings.precision))
getHash(ahashSettings, precisionMap(ahashSettings.precision), AHash.getHash)
else 0l
val dhash: Long = if (dhashSettings.use && precisionMap.contains(dhashSettings.precision))
getHash(dhashSettings, precisionMap(dhashSettings.precision), DHash.getHash)
else 0l
val phash: Long = if (phashSettings.use && precisionMap.contains(phashSettings.precision))
getHash(phashSettings, precisionMap(phashSettings.precision), PHash.getHash)
else 0l
val sha1: String = getSHA1(imagePath)
val hashes = new ImageHash(ahash, dhash, phash, sha1)
debug(s"Generated hashes: $hashes")
hashes
}
def getSHA1(filePath: String): String = {
managed(new FileInputStream(filePath)) acquireAndGet {
input => DigestUtils.sha1Hex(input)
}
}
def getPrecisionSet(ahashSettings: HashSetting, dhashSettings: HashSetting, phashSettings: HashSetting): Set[Int] = {
Set(
if (ahashSettings.use) Option(ahashSettings.precision) else None,
if (dhashSettings.use) Option(dhashSettings.precision) else None,
if (phashSettings.use) Option(phashSettings.precision) else None
).flatten
}
def getImageData(precision: Int, image: BufferedImage, alreadyGray: Boolean): Array[Array[Int]] = {
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
grayImage = ImageUtil.convertToGray(image)
}
val resizedImage = ImageUtil.resize(grayImage, precision, forced = true)
ImageUtil.getImageData(resizedImage)
}
/**
* Simpler function that only works with the imageData and the hashFunction
*
* @param hashSettings
* @param imageData
* @param hashFunction
* @return
*/
def getHash(hashSettings: HashSetting, imageData: Array[Array[Int]], hashFunction: Array[Array[Int]] => Long): Long = {
if (hashSettings.use) {
val hashResult = hashFunction(imageData)
trace(s"${hashSettings.name} result: $hashResult")
hashResult
} else {
0l
}
}
def getAhash(hashSettings: HashSetting, image: BufferedImage, alreadyGray: Boolean = false): Long = {
getHash(hashSettings, image, alreadyGray, AHash.getHash)
}
/**
* Internal function to retrieve the hash from a processed image, for a given hash function
*
* @param hashSettings A HashSettings that is used for the processing parameters of the Image
* @param image BufferedImage representing the image data to calculate the hash for
* @param alreadyGray Indicator that the grayscale processing has already been applied to the incoming image
* @param hashFunction The function accepting an two dimensional array of Int and returning the hash of the data
* @return
*/
def getHash(hashSettings: HashSetting, image: BufferedImage, alreadyGray: Boolean = false, hashFunction: Array[Array[Int]] => Long): Long = {
if (hashSettings.use) {
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
grayImage = ImageUtil.convertToGray(image)
}
val resizedImage = ImageUtil.resize(grayImage, hashSettings.precision, forced = true)
val hashResult = hashFunction(ImageUtil.getImageData(resizedImage))
trace(s"${hashSettings.name} result: $hashResult")
hashResult
} else {
trace(s"${hashSettings.name} result: DISABLED")
0l
}
}
def getDhash(hashSettings: HashSetting, image: BufferedImage, alreadyGray: Boolean = false): Long = {
getHash(hashSettings, image, alreadyGray, DHash.getHash)
}
def getPhash(hashSettings: HashSetting, image: BufferedImage, alreadyGray: Boolean = false): Long = {
getHash(hashSettings, image, alreadyGray, PHash.getHash)
}
def getMD5(filePath: String): String = {
managed(new FileInputStream(filePath)) acquireAndGet {
input => DigestUtils.md5Hex(input)
}
}
def areHashSimilar(hashSettings: HashSetting, hash1: Long, hash2: Long): Boolean = {
val tolerance = hashSettings.tolerance
val distance = HammingUtil.getDistance(hash1, hash2)
if (distance <= tolerance) true else false
}
def areImageHashesSimilar(ahashSettings: HashSetting,
dhashSettings: HashSetting,
phashSettings: HashSetting,
imageHash1: ImageHash,
imageHash2: ImageHash): Boolean = {
val weightedHammingMean = getWeightedHashSimilarity(ahashSettings, dhashSettings, phashSettings, imageHash1, imageHash2)
val weightedToleranceMean = getWeightedHashTolerance(ahashSettings, dhashSettings, phashSettings)
if (weightedHammingMean <= weightedToleranceMean) true else false
}
def getWeightedHashSimilarity(ahashSettings: HashSetting,
dhashSettings: HashSetting,
phashSettings: HashSetting,
imageHash1: ImageHash,
imageHash2: ImageHash): Float = {
val (weightedHammingTotal1, methodsTotal1) = sumWeightedHammingAndMethodCount(ahashSettings, imageHash1.ahash, imageHash2.ahash, 0f, 0)
val (weightedHammingTotal2, methodsTotal2) = sumWeightedHammingAndMethodCount(dhashSettings, imageHash1.dhash, imageHash2.dhash, weightedHammingTotal1, methodsTotal1)
val (weightedHammingTotalFinal, methodsTotalFinal) = sumWeightedHammingAndMethodCount(phashSettings, imageHash1.phash, imageHash2.phash, weightedHammingTotal2, methodsTotal2)
val weightedHammingMean = weightedHammingTotalFinal / methodsTotalFinal
debug(s"Calculated Weighted HammingUtil Mean: $weightedHammingMean")
weightedHammingMean
}
def sumWeightedHammingAndMethodCount(hashSettings: HashSetting, hash1: Long, hash2: Long, weightedHamming: Float, methodCount: Int): (Float, Int) = {
val (newWeightedHamming, newMethodCount) = getWeightedHamming(hashSettings, hash1, hash2)
(weightedHamming + newWeightedHamming, methodCount + newMethodCount)
}
def getWeightedHamming(hashSettings: HashSetting, hash1: Long, hash2: Long): (Float, Int) = {
if (hashSettings.use) {
val hamming = HammingUtil.getDistance(hash1, hash2)
trace(s"${hashSettings.name} Hamming: hash1: $hash1 hash2: $hash2 tolerance: ${hashSettings.tolerance} hamming distance: $hamming weight: ${hashSettings.weight}")
(hamming * hashSettings.weight, 1)
} else {
trace(s"${hashSettings.name} Hamming: DISABLED")
(0f, 0)
}
}
def getWeightedHashTolerance(ahashSettings: HashSetting,
dhashSettings: HashSetting,
phashSettings: HashSetting): Float = {
val (weightedToleranceTotal1, methodsTotal1) = sumWeightedToleranceAndMethodCount(ahashSettings, 0f, 0)
val (weightedToleranceTotal2, methodsTotal2) = sumWeightedToleranceAndMethodCount(dhashSettings, weightedToleranceTotal1, methodsTotal1)
val (weightedToleranceTotalFinal, methodsTotalFinal) = sumWeightedToleranceAndMethodCount(phashSettings, weightedToleranceTotal2, methodsTotal2)
val weightedTolerance = weightedToleranceTotalFinal / methodsTotalFinal
debug(s"Calculated Weighted Tolerance: $weightedTolerance")
weightedTolerance
}
def sumWeightedToleranceAndMethodCount(hashSettings: HashSetting, weightedTolerance: Float, methodCount: Int): (Float, Int) = {
val (newWeightedTolerance, newMethodCount) = getWeightedTolerance(hashSettings)
(weightedTolerance + newWeightedTolerance, methodCount + newMethodCount)
}
def getWeightedTolerance(hashSettings: HashSetting): (Float, Int) = {
if (hashSettings.use) {
val weightedTolerance = hashSettings.tolerance * hashSettings.weight
trace(s"${hashSettings.name} Tolerance: ${hashSettings.tolerance} Current Weighted Tolerance: $weightedTolerance")
(weightedTolerance, 1)
} else {
trace(s"${hashSettings.name} Tolerance: DISABLED")
(0f, 0)
}
}
} }

8
hash/src/main/scala/com/sothr/imagetools/hash/HashSetting.scala

@ -0,0 +1,8 @@
package com.sothr.imagetools.hash
class HashSetting(val name: String,
val use: Boolean,
val precision: Int,
val tolerance: Int,
val weight: Float) {
}

25
hash/src/main/scala/com/sothr/imagetools/hash/ImageHash.scala

@ -0,0 +1,25 @@
package com.sothr.imagetools.hash
import grizzled.slf4j.Logging
class ImageHash(val ahash: Long,
val dhash: Long,
val phash: Long,
val fileHash: String) extends Serializable with Logging {
def cloneHashes: ImageHash = {
new ImageHash(ahash, dhash, phash, fileHash)
}
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
result = 2 * result + (this.phash ^ (this.phash >>> 32)).toInt
result
}
override def toString: String = {
s"fileHash: $fileHash ahash: $ahash dhash: $dhash phash: $phash"
}
}

8
hash/src/main/scala/com/sothr/imagetools/hash/dto/HashSettingDTO.scala

@ -1,8 +0,0 @@
package com.sothr.imagetools.hash.dto
class HashSettingDTO(
val use: Boolean,
val precision: Int,
val tolerance: Int,
val weight: Float) {
}

46
hash/src/main/scala/com/sothr/imagetools/hash/dto/ImageHashDTO.scala

@ -1,46 +0,0 @@
package com.sothr.imagetools.hash.dto
import grizzled.slf4j.Logging
class ImageHashDTO(var ahash: Long, var dhash: Long, var phash: Long, var fileHash: String) extends Serializable with Logging {
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 getFileHash: String = fileHash
def setFileHash(hash: String) = {
fileHash = hash
}
def cloneHashes: ImageHashDTO = {
new ImageHashDTO(ahash, dhash, phash, fileHash)
}
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
result = 2 * result + (this.phash ^ (this.phash >>> 32)).toInt
result
}
override def toString: String = {
s"fileHash: $fileHash ahash: $ahash dhash: $dhash phash: $phash"
}
}

83
hash/src/main/scala/com/sothr/imagetools/hash/type/AHash.scala

@ -3,49 +3,50 @@ package com.sothr.imagetools.hash.`type`
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
/** /**
* Speedy AHash Perceptual Hashing
* Uses the average value of the pixels as a baseline
*
* Created by dev on 1/22/14.
*/
* Speedy AHash Perceptual Hashing
* Uses the average value of the pixels as a baseline to determine the structure of the image
*
* Created by dev on 1/22/14.
*/
object AHash extends PerceptualHasher with Logging { object AHash extends PerceptualHasher with Logging {
def getHash(imageData: Array[Array[Int]]): Long = {
trace("Generating AHash")
val width = imageData.length
val height = imageData(0).length
trace(s"Image data size: ${width}x$height")
def getHash(imageData: Array[Array[Int]]): Long = {
trace("Generating AHash")
val width = imageData.length
val height = imageData(0).length
trace(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)
/*
Average Pixel Calculation
*/
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
}
/*
For each pixel, if it is at or above the average, store it as a one, else store it as a zero
*/
var hash = 0L
for (row <- 0 until height by 2) {
for (col <- 0 until width by 1) {
hash <<= 1
val pixel = imageData(row)(col)
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
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
}
} }

96
hash/src/main/scala/com/sothr/imagetools/hash/type/DHash.scala

@ -3,55 +3,57 @@ package com.sothr.imagetools.hash.`type`
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
/** /**
* DHash algorithm class
*
* Created by Drew on 1/22/14.
*/
* DHash algorithm object
*
* Tracks the differences between pixels in a pattern as the basis of similarity
*
* Created by Drew on 1/22/14.
*/
object DHash extends PerceptualHasher with Logging { object DHash extends PerceptualHasher with Logging {
def getHash(imageData: Array[Array[Int]]): Long = {
trace("Generating DHash")
val width = imageData.length
val height = imageData(0).length
trace(s"Image data size: ${width}x$height")
def getHash(imageData: Array[Array[Int]]): Long = {
trace("Generating DHash")
val width = imageData.length
val height = imageData(0).length
trace(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)
if (height % 2 == 0) {
previousPixel = imageData(height - 1)(0)
previousLocation = (height - 1, 0)
}
//calculate dhash
var hash = 0L
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)
}
for (row <- 0 until height by 2) {
//process each column
for (col <- 0 until width by 1) {
hash <<= 1
val pixel = imageData(row)(col)
//debug(s"previousPixel: $previousPixel currentPixel: $pixel previousLocation: $previousLocation currentLocation: (${row},${col})")
//binary of the current bit based on whether the value
//of the current pixel is greater or equal to the previous pixel
if (pixel >= previousPixel) hash |= 1 else hash |= 0
//debug(s"(${row},${col})=$pixel hash=${hash.toBinaryString}")
previousPixel = pixel
previousLocation = (row, col)
}
for (row <- 0 until height by 2) {
//process each column
for (col <- 0 until width by 1) {
hash <<= 1
val pixel = imageData(row)(col)
//debug(s"previousPixel: $previousPixel currentPixel: $pixel previousLocation: $previousLocation currentLocation: (${row},${col})")
//binary of the current bit based on whether the value
//of the current pixel is greater or equal to the previous pixel
if (pixel >= previousPixel) hash |= 1 else hash |= 0
//debug(s"(${row},${col})=$pixel hash=${hash.toBinaryString}")
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)
}
}
}
debug(s"Computed DHash: $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)
//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")
hash
}
} }

113
hash/src/main/scala/com/sothr/imagetools/hash/type/PHash.scala

@ -4,69 +4,66 @@ import edu.emory.mathcs.jtransforms.dct.FloatDCT_2D
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
/** /**
* Complex perceptual hash
* Uses FFT to get
*
* Created by dev on 1/22/14.
*/
* Complex perceptual hash
* Uses FFT to get structural data out of the image, which can represent the perception of similarity
*
* Created by dev on 1/22/14.
*/
object PHash extends PerceptualHasher with Logging { 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
trace(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
trace(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
}
}
//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)
//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
}
/*
For each pixel, if it is at or above the mean, store it as a one, else store it as a zero
*/
var hash = 0L
for (row <- 0 until dctDataHeight by 2) {
for (col <- 0 until dctDataWidth by 1) {
hash <<= 1
val pixel = imageDataFloat(row)(col)
//
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
}
} }

10
hash/src/main/scala/com/sothr/imagetools/hash/type/PerceptualHasher.scala

@ -1,12 +1,12 @@
package com.sothr.imagetools.hash.`type` package com.sothr.imagetools.hash.`type`
/** /**
* Interface for perceptual hashing
*
* Created by drew on 1/22/14.
*/
* Interface for perceptual hashing
*
* Created by drew on 1/22/14.
*/
trait PerceptualHasher { trait PerceptualHasher {
def getHash(imageData: Array[Array[Int]]): Long
def getHash(imageData: Array[Array[Int]]): Long
} }

25
hash/src/main/scala/com/sothr/imagetools/hash/util/HammingUtil.scala

@ -2,17 +2,16 @@ package com.sothr.imagetools.hash.util
object HammingUtil { object HammingUtil {
/**
* Calculate the hamming distance between two longs
*
* @param hash1 The first hash to compare
* @param hash2 The second hash to compare
* @return
*/
def getDistance(hash1: Long, hash2: Long): Int = {
//The XOR of hash1 and hash2 is converted to a binary string
//then the number of '1's is counted. This is the hamming distance
(hash1 ^ hash2).toBinaryString.count(_ == '1')
}
/**
* Calculate the hamming distance between two longs
*
* @param hash1 The first hash to compare
* @param hash2 The second hash to compare
* @return
*/
def getDistance(hash1: Long, hash2: Long): Int = {
//The XOR of hash1 and hash2 is converted to a binary string
//then the number of '1's is counted. This is the hamming distance
(hash1 ^ hash2).toBinaryString.count(_ == '1')
}
} }

201
hash/src/main/scala/com/sothr/imagetools/hash/util/ImageUtil.scala

@ -7,111 +7,110 @@ import net.coobird.thumbnailator.Thumbnails
object ImageUtil extends Logging { object ImageUtil extends Logging {
/**
* Quickly convert an image to grayscale
*
* @param image image to convert to greyscale
* @return
*/
def convertToGray(image: BufferedImage): BufferedImage = {
val grayImage = new BufferedImage(image.getWidth, image.getHeight, BufferedImage.TYPE_BYTE_GRAY)
/**
* Quickly convert an image to grayscale
*
* @param image image to convert to greyscale
* @return
*/
def convertToGray(image: BufferedImage): BufferedImage = {
val grayImage = new BufferedImage(image.getWidth, image.getHeight, BufferedImage.TYPE_BYTE_GRAY)
val op = new ColorConvertOp(
image.getColorModel.getColorSpace,
grayImage.getColorModel.getColorSpace,
null
)
val op = new ColorConvertOp(
image.getColorModel.getColorSpace,
grayImage.getColorModel.getColorSpace,
null
)
op.filter(image, grayImage)
}
op.filter(image, grayImage)
}
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
} else {
Thumbnails.of(image).size(size, size).asBufferedImage
}
}
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
} else {
Thumbnails.of(image).size(size, size).asBufferedImage
}
}
/**
* Get the raw data for an image
* Convert a buffered image into a 2d pixel data array
*
* @param image image to convert without using RGB
* @return
*/
def getImageData(image: BufferedImage): Array[Array[Int]] = {
/**
* Get the raw data for an image
* Convert a buffered image into a 2d pixel data array
*
* @param image image to convert without using RGB
* @return
*/
def getImageData(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 hasAlphaChannel = image.getAlphaRaster != null
//debug(s"Converting image to 2d. width:$width height:$height")
val result = Array.ofDim[Int](height, width)
if (isSingleChannel) {
//debug(s"Processing Single Channel Image")
val pixelLength = 1
var row = 0
var col = 0
//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
//debug(s"Pixel data: $argb")
result(row)(col) = argb
col += 1
if (col == width) {
col = 0
row += 1
}
}
}
else if (hasAlphaChannel) {
//debug(s"Processing Four Channel Image")
val pixelLength = 4
var row = 0
var col = 0
//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
argb += pixels(pixel).toInt << 24 //alpha
argb += pixels(pixel + 1).toInt //blue
argb += pixels(pixel + 2).toInt << 8 //green
argb += pixels(pixel + 3).toInt << 16 //red
result(row)(col) = argb
col += 1
if (col == width) {
col = 0
row += 1
}
}
} else {
//debug(s"Processing Three Channel Image")
val pixelLength = 3
var row = 0
var col = 0
//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
argb += -16777216; // 255 alpha
argb += pixels(pixel).toInt //blue
argb += pixels(pixel + 1).toInt << 8 //green
argb += pixels(pixel + 2).toInt << 16 //red
result(row)(col) = argb
col += 1
if (col == width) {
col = 0
row += 1
}
}
}
result
}
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 hasAlphaChannel = image.getAlphaRaster != null
//debug(s"Converting image to 2d. width:$width height:$height")
val result = Array.ofDim[Int](height, width)
if (isSingleChannel) {
//debug(s"Processing Single Channel Image")
val pixelLength = 1
var row = 0
var col = 0
//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
//debug(s"Pixel data: $argb")
result(row)(col) = argb
col += 1
if (col == width) {
col = 0
row += 1
}
}
}
else if (hasAlphaChannel) {
//debug(s"Processing Four Channel Image")
val pixelLength = 4
var row = 0
var col = 0
//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
argb += pixels(pixel).toInt << 24 //alpha
argb += pixels(pixel + 1).toInt //blue
argb += pixels(pixel + 2).toInt << 8 //green
argb += pixels(pixel + 3).toInt << 16 //red
result(row)(col) = argb
col += 1
if (col == width) {
col = 0
row += 1
}
}
} else {
//debug(s"Processing Three Channel Image")
val pixelLength = 3
var row = 0
var col = 0
//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
argb += -16777216; // 255 alpha
argb += pixels(pixel).toInt //blue
argb += pixels(pixel + 1).toInt << 8 //green
argb += pixels(pixel + 2).toInt << 16 //red
result(row)(col) = argb
col += 1
if (col == width) {
col = 0
row += 1
}
}
}
result
}
} }

49
hash/src/main/scala/com/sothr/imagetools/hash/util/TimingUtil.scala

@ -4,32 +4,31 @@ import grizzled.slf4j.Logging
trait TimingUtil extends Logging { trait TimingUtil extends Logging {
def time[R](block: => R): R = {
val t0 = System.currentTimeMillis
val result = block // call-by-name
val t1 = System.currentTimeMillis
debug("Elapsed time: " + (t1 - t0) + "ms")
result
}
def time[R](block: => R): R = {
val t0 = System.currentTimeMillis
val result = block // call-by-name
val t1 = System.currentTimeMillis
debug("Elapsed time: " + (t1 - t0) + "ms")
result
}
def getTime[R](block: => R): Long = {
val t0 = System.currentTimeMillis
val result = block // call-by-name
val t1 = System.currentTimeMillis
debug("Elapsed time: " + (t1 - t0) + "ms")
t1 - t0
}
def getTime[R](block: => R): Long = {
val t0 = System.currentTimeMillis
val result = block // call-by-name
val t1 = System.currentTimeMillis
debug("Elapsed time: " + (t1 - t0) + "ms")
t1 - t0
}
def getMean(times: Long*): Long = {
getMean(times.toArray[Long])
}
def getMean(times: Array[Long]): Long = {
var ag = 0L
for (i <- times.indices) {
ag += times(i)
}
ag / times.length
}
def getMean(times: Long*): Long = {
getMean(times.toArray[Long])
}
def getMean(times: Array[Long]): Long = {
var ag = 0L
for (i <- times.indices) {
ag += times(i)
}
ag / times.length
}
} }

9
hash/src/test/scala/com/sothr/imagetools/hash/BaseTest.scala

@ -1,9 +0,0 @@
package com.sothr.imagetools.hash
import com.sothr.imagetools.hash.util.TimingUtil
import grizzled.slf4j.Logging
import org.scalatest.{BeforeAndAfter, FunSuite, Inside, Inspectors, Matchers, OptionValues}
abstract class BaseTest extends FunSuite with Matchers with OptionValues with Inside with Inspectors with BeforeAndAfter with Logging with TimingUtil {
}

93
hash/src/test/scala/com/sothr/imagetools/hash/HashServiceAHashTest.scala

@ -0,0 +1,93 @@
package com.sothr.imagetools.hash
import java.io.File
import javax.imageio.ImageIO
import scala.collection.mutable
class HashServiceAHashTest extends HashServiceBaseTest {
def ahashTestCase(filePath: String): Long = {
val sample = new File(filePath)
val image = ImageIO.read(sample)
HashService.getAhash(ahashSetting, image)
}
test("Calculate AHash Large Sample Image 1") {
debug("Starting 'Calculate AHash Large Sample Image 1' test")
val sample = new File(LargeSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getAhash(ahashSetting, image)
debug(s"Testing that $hash = 36070299219713907L")
assert(hash == 36070299219713907L)
}
test("Calculate AHash Medium Sample Image 1") {
debug("Starting 'Calculate AHash Medium Sample Image 1' test")
val sample = new File(MediumSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getAhash(ahashSetting, image)
debug(s"Testing that $hash = 36070299219713907L")
assert(hash == 36070299219713907L)
}
test("Calculate AHash Small Sample Image 1") {
debug("Starting 'Calculate AHash Small Sample Image 1' test")
val sample = new File(SmallSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getAhash(ahashSetting, image)
debug(s"Testing that $hash = 36070299219713907L")
assert(hash == 36070299219713907L)
}
test("AHash Of Large, Medium, And Small Sample 1 Must Be Similar") {
val largeHash = ahashTestCase(LargeSampleImage1)
val mediumHash = ahashTestCase(MediumSampleImage1)
val smallHash = ahashTestCase(SmallSampleImage1)
assert(HashService.areHashSimilar(ahashSetting, largeHash, mediumHash))
assert(HashService.areHashSimilar(ahashSetting, largeHash, smallHash))
assert(HashService.areHashSimilar(ahashSetting, mediumHash, smallHash))
}
test("Benchmark AHash") {
info("Benchmarking AHash")
info("AHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (_ <- 0 until benchmarkRuns) {
time += getTime {
ahashTestCase(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 (_ <- 0 until benchmarkRuns) {
time += getTime {
ahashTestCase(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 (_ <- 0 until benchmarkRuns) {
time += getTime {
ahashTestCase(SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
time.clear()
assert(true)
}
}

21
hash/src/test/scala/com/sothr/imagetools/hash/HashServiceBaseTest.scala

@ -0,0 +1,21 @@
package com.sothr.imagetools.hash
import com.sothr.imagetools.hash.util.TimingUtil
import grizzled.slf4j.Logging
import org.scalatest.{BeforeAndAfter, FunSuite, Inside, Inspectors, Matchers, OptionValues}
abstract class HashServiceBaseTest extends FunSuite with Matchers with OptionValues with Inside with Inspectors with BeforeAndAfter with Logging with TimingUtil {
// Define the number of runs the benchmarking tests should use
val benchmarkRuns = 10
// The Standard Parameters Used To Benchmark Perceptual hashes
val ahashSetting = new HashSetting("AHash", true, 8, 8, 0.75f)
val dhashSetting = new HashSetting("DHash", true, 8, 8, 0.85f)
val phashSetting = new HashSetting("PHash", true, 32, 8, 1.0f)
// Base Test Images
val LargeSampleImage1 = "sample/sample_01_large.jpg"
val MediumSampleImage1 = "sample/sample_01_medium.jpg"
val SmallSampleImage1 = "sample/sample_01_small.jpg"
}

110
hash/src/test/scala/com/sothr/imagetools/hash/HashServiceDHashTest.scala

@ -0,0 +1,110 @@
package com.sothr.imagetools.hash
import java.io.File
import javax.imageio.ImageIO
import com.sothr.imagetools.hash.`type`.DHash
import scala.collection.mutable
class HashServiceDHashTest extends HashServiceBaseTest {
def dhashTestCase(filePath: String): Long = {
val sample = new File(filePath)
val image = ImageIO.read(sample)
HashService.getDhash(dhashSetting, image)
}
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 hash = DHash.getHash(testData)
debug(s"Hash of test array: $hash")
assert(hash == Long.MaxValue)
}
test("Calculate DHash Large Sample Image 1") {
debug("Starting 'Calculate DHash Large Sample Image 1' test")
val sample = new File(LargeSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getDhash(dhashSetting, image)
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(MediumSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getDhash(dhashSetting, image)
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(SmallSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getDhash(dhashSetting, image)
debug(s"Testing that $hash = 4004383623972821843L")
assert(hash == 4004383623972821843L)
}
test("DHash Of Large, Medium, And Small Sample 1 Must Be Similar") {
val largeHash = dhashTestCase(LargeSampleImage1)
val mediumHash = dhashTestCase(MediumSampleImage1)
val smallHash = dhashTestCase(SmallSampleImage1)
assert(HashService.areHashSimilar(dhashSetting, largeHash, mediumHash))
assert(HashService.areHashSimilar(dhashSetting, largeHash, smallHash))
assert(HashService.areHashSimilar(dhashSetting, mediumHash, smallHash))
}
test("Benchmark DHash") {
info("Benchmarking DHash")
info("DHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (_ <- 0 until benchmarkRuns) {
time += getTime {
dhashTestCase(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 (_ <- 0 until benchmarkRuns) {
time += getTime {
dhashTestCase(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 (_ <- 0 until benchmarkRuns) {
time += getTime {
dhashTestCase(SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
time.clear()
assert(true)
}
}

67
hash/src/test/scala/com/sothr/imagetools/hash/HashServiceMD5Test.scala

@ -0,0 +1,67 @@
package com.sothr.imagetools.hash
import scala.collection.mutable
class HashServiceMD5Test extends HashServiceBaseTest {
def md5TestCase(filePath: String): String = {
HashService.getMD5(filePath)
}
test("Calculate MD5 Large Sample Image 1") {
debug("Starting 'Calculate MD5 Large Sample Image 1' test")
val hash = HashService.getMD5(LargeSampleImage1)
debug(s"Testing that $hash = 3fbccfd5faf3f991435b827ee5961862")
assert(hash == "3fbccfd5faf3f991435b827ee5961862")
}
test("Calculate MD5 Medium Sample Image 1") {
debug("Starting 'Calculate MD5 Medium Sample Image 1' test")
val hash = HashService.getMD5(MediumSampleImage1)
debug(s"Testing that $hash = a95e2cc4610307eb957e9c812429c53e")
assert(hash == "a95e2cc4610307eb957e9c812429c53e")
}
test("Calculate MD5 Small Sample Image 1") {
debug("Starting 'Calculate MD5 Small Sample Image 1' test")
val hash = HashService.getMD5(SmallSampleImage1)
debug(s"Testing that $hash = b137131bd55896c747286e4d247b845e")
assert(hash == "b137131bd55896c747286e4d247b845e")
}
test("Benchmark MD5") {
info("Benchmarking MD5")
info("MD5 Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (_ <- 0 until benchmarkRuns) {
time += getTime {
md5TestCase(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 (_ <- 0 until benchmarkRuns) {
time += getTime {
md5TestCase(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 (_ <- 0 until benchmarkRuns) {
time += getTime {
md5TestCase(SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
time.clear()
assert(true)
}
}

93
hash/src/test/scala/com/sothr/imagetools/hash/HashServicePHashTest.scala

@ -0,0 +1,93 @@
package com.sothr.imagetools.hash
import java.io.File
import javax.imageio.ImageIO
import scala.collection.mutable
class HashServicePHashTest extends HashServiceBaseTest {
def phashTestCase(filePath: String): Long = {
val sample = new File(filePath)
val image = ImageIO.read(sample)
HashService.getPhash(phashSetting, image)
}
test("Calculate PHash Large Sample Image 1") {
debug("Starting 'Calculate PHash Large Sample Image 1' test")
val sample = new File(LargeSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getPhash(phashSetting, image)
debug(s"Testing that $hash = -9154554603604154117L")
assert(hash == -9154554603604154117L)
}
test("Calculate PHash Medium Sample Image 1") {
debug("Starting 'Calculate PHash Medium Sample Image 1' test")
val sample = new File(MediumSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getPhash(phashSetting, image)
debug(s"Testing that $hash = -9154589787976242949L")
assert(hash == -9154589787976242949L)
}
test("Calculate PHash Small Sample Image 1") {
debug("Starting 'Calculate PHash Small Sample Image 1' test")
val sample = new File(SmallSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getPhash(phashSetting, image)
debug(s"Testing that $hash = -9154589787976242949L")
assert(hash == -9154589787976242949L)
}
test("PHash Of Large, Medium, And Small Sample 1 Must Be Similar") {
val largeHash = phashTestCase(LargeSampleImage1)
val mediumHash = phashTestCase(MediumSampleImage1)
val smallHash = phashTestCase(SmallSampleImage1)
assert(HashService.areHashSimilar(phashSetting, largeHash, mediumHash))
assert(HashService.areHashSimilar(phashSetting, largeHash, smallHash))
assert(HashService.areHashSimilar(phashSetting, mediumHash, smallHash))
}
test("Benchmark PHash") {
info("Benchmarking PHash")
info("PHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (_ <- 0 until benchmarkRuns) {
time += getTime {
phashTestCase(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 (_ <- 0 until benchmarkRuns) {
time += getTime {
phashTestCase(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 (_ <- 0 until benchmarkRuns) {
time += getTime {
phashTestCase(SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
time.clear()
assert(true)
}
}

67
hash/src/test/scala/com/sothr/imagetools/hash/HashServiceSHA1Test.scala

@ -0,0 +1,67 @@
package com.sothr.imagetools.hash
import scala.collection.mutable
class HashServiceSHA1Test extends HashServiceBaseTest {
def sha1TestCase(filePath: String): String = {
HashService.getSHA1(filePath)
}
test("Calculate SHA1 Large Sample Image 1") {
debug("Starting 'Calculate SHA1 Large Sample Image 1' test")
val hash = HashService.getSHA1(LargeSampleImage1)
debug(s"Testing that $hash = 4beb6f2d852b75a313863916a1803ebad13a3196")
assert(hash == "4beb6f2d852b75a313863916a1803ebad13a3196")
}
test("Calculate SHA1 Medium Sample Image 1") {
debug("Starting 'Calculate SHA1 Medium Sample Image 1' test")
val hash = HashService.getSHA1(MediumSampleImage1)
debug(s"Testing that $hash = edc718ce8e3556a39592ffdc214d0f636529be9f")
assert(hash == "edc718ce8e3556a39592ffdc214d0f636529be9f")
}
test("Calculate SHA1 Small Sample Image 1") {
debug("Starting 'Calculate SHA1 Small Sample Image 1' test")
val hash = HashService.getSHA1(SmallSampleImage1)
debug(s"Testing that $hash = 1a91d2b5327f0aad258419f76b87d4c0bc343443")
assert(hash == "1a91d2b5327f0aad258419f76b87d4c0bc343443")
}
test("Benchmark SHA1") {
info("Benchmarking SHA1")
info("SHA1 Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (_ <- 0 until benchmarkRuns) {
time += getTime {
sha1TestCase(LargeSampleImage1)
}
}
val largeMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("SHA1 Medium Image 1824x1368")
for (_ <- 0 until benchmarkRuns) {
time += getTime {
sha1TestCase(MediumSampleImage1)
}
}
val mediumMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("SHA1 Small Image 912x684")
for (_ <- 0 until benchmarkRuns) {
time += getTime {
sha1TestCase(SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
time.clear()
assert(true)
}
}

578
hash/src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala

@ -1,10 +1,12 @@
package com.sothr.imagetools.hash package com.sothr.imagetools.hash
import java.io.File import java.io.File
import java.util.NoSuchElementException
import javax.imageio.ImageIO import javax.imageio.ImageIO
import com.sothr.imagetools.hash.`type`.DHash
import com.sothr.imagetools.hash.dto.{HashSettingDTO, ImageHashDTO}
import com.sothr.imagetools.hash.HashService.getHash
import com.sothr.imagetools.hash.`type`.{AHash, DHash, PHash}
import com.sothr.imagetools.hash.util.ImageUtil
import scala.collection.mutable import scala.collection.mutable
@ -13,453 +15,127 @@ import scala.collection.mutable
* *
* Created by dev on 1/23/14. * Created by dev on 1/23/14.
*/ */
class HashServiceTest extends BaseTest {
// Define the number of runs the benchmarking tests should use
val benchmarkRuns = 10
val ahashSetting = new HashSettingDTO(true, 8, 8, 0.75f)
val dhashSetting = new HashSettingDTO(true, 8, 8, 0.85f)
val phashSetting = new HashSettingDTO(true, 32, 8, 1.0f)
def dhashTestCase(filePath: String): Long = {
val sample = new File(filePath)
val image = ImageIO.read(sample)
HashService.getDhash(dhashSetting, image)
}
test("Benchmark DHash") {
info("Benchmarking DHash")
info("DHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (_ <- 0 until benchmarkRuns) {
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 (_ <- 0 until benchmarkRuns) {
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 (_ <- 0 until benchmarkRuns) {
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")
time.clear()
assert(true)
}
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 hash = DHash.getHash(testData)
debug(s"Hash of test array: $hash")
assert(hash == Long.MaxValue)
}
test("Calculate DHash Large Sample Image 1") {
debug("Starting 'Calculate DHash Large Sample Image 1' test")
val sample = new File(TestParams.LargeSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getDhash(dhashSetting, image)
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)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getDhash(dhashSetting, image)
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)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getDhash(dhashSetting, image)
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.areHashSimilar(dhashSetting, largeHash, mediumHash))
assert(HashService.areHashSimilar(dhashSetting, largeHash, smallHash))
assert(HashService.areHashSimilar(dhashSetting, mediumHash, smallHash))
}
def ahashTestCase(filePath: String): Long = {
val sample = new File(filePath)
val image = ImageIO.read(sample)
HashService.getAhash(ahashSetting, image)
}
test("Benchmark AHash") {
info("Benchmarking AHash")
info("AHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (_ <- 0 until benchmarkRuns) {
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 (_ <- 0 until benchmarkRuns) {
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 (_ <- 0 until benchmarkRuns) {
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")
time.clear()
assert(true)
}
test("Calculate AHash Large Sample Image 1") {
debug("Starting 'Calculate AHash Large Sample Image 1' test")
val sample = new File(TestParams.LargeSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getAhash(ahashSetting, image)
debug(s"Testing that $hash = 36070299219713907L")
assert(hash == 36070299219713907L)
}
test("Calculate AHash Medium Sample Image 1") {
debug("Starting 'Calculate AHash Medium Sample Image 1' test")
val sample = new File(TestParams.MediumSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getAhash(ahashSetting, image)
debug(s"Testing that $hash = 36070299219713907L")
assert(hash == 36070299219713907L)
}
test("Calculate AHash Small Sample Image 1") {
debug("Starting 'Calculate AHash Small Sample Image 1' test")
val sample = new File(TestParams.SmallSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getAhash(ahashSetting, image)
debug(s"Testing that $hash = 36070299219713907L")
assert(hash == 36070299219713907L)
}
test("AHash Of Large, Medium, And Small Sample 1 Must Be Similar") {
val largeHash = ahashTestCase(TestParams.LargeSampleImage1)
val mediumHash = ahashTestCase(TestParams.MediumSampleImage1)
val smallHash = ahashTestCase(TestParams.SmallSampleImage1)
assert(HashService.areHashSimilar(ahashSetting, largeHash, mediumHash))
assert(HashService.areHashSimilar(ahashSetting, largeHash, smallHash))
assert(HashService.areHashSimilar(ahashSetting, mediumHash, smallHash))
}
def phashTestCase(filePath: String): Long = {
val sample = new File(filePath)
val image = ImageIO.read(sample)
HashService.getPhash(phashSetting, image)
}
test("Benchmark PHash") {
info("Benchmarking PHash")
info("PHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (_ <- 0 until benchmarkRuns) {
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 (_ <- 0 until benchmarkRuns) {
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 (_ <- 0 until benchmarkRuns) {
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")
time.clear()
assert(true)
}
test("Calculate PHash Large Sample Image 1") {
debug("Starting 'Calculate PHash Large Sample Image 1' test")
val sample = new File(TestParams.LargeSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getPhash(phashSetting, image)
debug(s"Testing that $hash = -9154554603604154117L")
assert(hash == -9154554603604154117L)
}
test("Calculate PHash Medium Sample Image 1") {
debug("Starting 'Calculate PHash Medium Sample Image 1' test")
val sample = new File(TestParams.MediumSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getPhash(phashSetting, image)
debug(s"Testing that $hash = -9154589787976242949L")
assert(hash == -9154589787976242949L)
}
test("Calculate PHash Small Sample Image 1") {
debug("Starting 'Calculate PHash Small Sample Image 1' test")
val sample = new File(TestParams.SmallSampleImage1)
debug(s"Testing File: ${sample.getAbsolutePath} exists: ${sample.exists}")
val image = ImageIO.read(sample)
debug(s"Image: width: ${image.getWidth} height: ${image.getHeight}")
val hash = HashService.getPhash(phashSetting, image)
debug(s"Testing that $hash = -9154589787976242949L")
assert(hash == -9154589787976242949L)
}
test("PHash Of Large, Medium, And Small Sample 1 Must Be Similar") {
val largeHash = phashTestCase(TestParams.LargeSampleImage1)
val mediumHash = phashTestCase(TestParams.MediumSampleImage1)
val smallHash = phashTestCase(TestParams.SmallSampleImage1)
assert(HashService.areHashSimilar(phashSetting, largeHash, mediumHash))
assert(HashService.areHashSimilar(phashSetting, largeHash, smallHash))
assert(HashService.areHashSimilar(phashSetting, mediumHash, smallHash))
}
def md5TestCase(filePath: String): String = {
HashService.getMD5(filePath)
}
test("Benchmark MD5") {
info("Benchmarking MD5")
info("MD5 Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (_ <- 0 until benchmarkRuns) {
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 (_ <- 0 until benchmarkRuns) {
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 (_ <- 0 until benchmarkRuns) {
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")
time.clear()
assert(true)
}
test("Calculate MD5 Large Sample Image 1") {
debug("Starting 'Calculate MD5 Large Sample Image 1' test")
val hash = HashService.getMD5(TestParams.LargeSampleImage1)
debug(s"Testing that $hash = 3fbccfd5faf3f991435b827ee5961862")
assert(hash == "3fbccfd5faf3f991435b827ee5961862")
}
test("Calculate MD5 Medium Sample Image 1") {
debug("Starting 'Calculate MD5 Medium Sample Image 1' test")
val hash = HashService.getMD5(TestParams.MediumSampleImage1)
debug(s"Testing that $hash = a95e2cc4610307eb957e9c812429c53e")
assert(hash == "a95e2cc4610307eb957e9c812429c53e")
}
test("Calculate MD5 Small Sample Image 1") {
debug("Starting 'Calculate MD5 Small Sample Image 1' test")
val hash = HashService.getMD5(TestParams.SmallSampleImage1)
debug(s"Testing that $hash = b137131bd55896c747286e4d247b845e")
assert(hash == "b137131bd55896c747286e4d247b845e")
}
def sha1TestCase(filePath: String): String = {
HashService.getSHA1(filePath)
}
test("Benchmark SHA1") {
info("Benchmarking SHA1")
info("SHA1 Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (_ <- 0 until benchmarkRuns) {
time += getTime {
sha1TestCase(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("SHA1 Medium Image 1824x1368")
for (_ <- 0 until benchmarkRuns) {
time += getTime {
sha1TestCase(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("SHA1 Small Image 912x684")
for (_ <- 0 until benchmarkRuns) {
time += getTime {
sha1TestCase(TestParams.SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
time.clear()
assert(true)
}
test("Calculate SHA1 Large Sample Image 1") {
debug("Starting 'Calculate SHA1 Large Sample Image 1' test")
val hash = HashService.getSHA1(TestParams.LargeSampleImage1)
debug(s"Testing that $hash = 4beb6f2d852b75a313863916a1803ebad13a3196")
assert(hash == "4beb6f2d852b75a313863916a1803ebad13a3196")
}
test("Calculate SHA1 Medium Sample Image 1") {
debug("Starting 'Calculate SHA1 Medium Sample Image 1' test")
val hash = HashService.getSHA1(TestParams.MediumSampleImage1)
debug(s"Testing that $hash = edc718ce8e3556a39592ffdc214d0f636529be9f")
assert(hash == "edc718ce8e3556a39592ffdc214d0f636529be9f")
}
test("Calculate SHA1 Small Sample Image 1") {
debug("Starting 'Calculate SHA1 Small Sample Image 1' test")
val hash = HashService.getSHA1(TestParams.SmallSampleImage1)
debug(s"Testing that $hash = 1a91d2b5327f0aad258419f76b87d4c0bc343443")
assert(hash == "1a91d2b5327f0aad258419f76b87d4c0bc343443")
}
def imageHashTestCase(filePath: String): ImageHashDTO = {
HashService.getImageHashes(ahashSetting, dhashSetting, phashSetting, filePath)
}
test("Benchmark getImageHashes") {
info("Benchmarking getImageHashes")
info("getImageHashes Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
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)
}
}
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)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
time.clear()
assert(true)
}
test("ImageHash Of Large, Medium, And Small Sample 1 Must Be Similar") {
val largeHash = imageHashTestCase(TestParams.LargeSampleImage1)
val mediumHash = imageHashTestCase(TestParams.MediumSampleImage1)
val smallHash = imageHashTestCase(TestParams.SmallSampleImage1)
assert(HashService.areImageHashesSimilar(ahashSetting, dhashSetting, phashSetting, largeHash, mediumHash))
assert(HashService.areImageHashesSimilar(ahashSetting, dhashSetting, phashSetting, largeHash, smallHash))
assert(HashService.areImageHashesSimilar(ahashSetting, dhashSetting, phashSetting, mediumHash, smallHash))
}
test("Calculate ImageHash Large Sample Image 1") {
debug("Starting 'Calculate ImageHash Large Sample Image 1' test")
val hash = HashService.getImageHashes(ahashSetting, dhashSetting, phashSetting, TestParams.LargeSampleImage1)
debug(s"Testing that ${hash.hashCode()} = -812844858")
assert(hash.hashCode == -812844858)
}
test("Calculate ImageHash Medium Sample Image 1") {
debug("Starting 'Calculate ImageHash Medium Sample Image 1' test")
val hash = HashService.getImageHashes(ahashSetting, dhashSetting, phashSetting, TestParams.MediumSampleImage1)
debug(s"Testing that ${hash.hashCode()} = -812836666")
assert(hash.hashCode == -812836666)
}
test("Calculate ImageHash Small Sample Image 1") {
debug("Starting 'Calculate ImageHash Small Sample Image 1' test")
val hash = HashService.getImageHashes(ahashSetting, dhashSetting, phashSetting, TestParams.SmallSampleImage1)
debug(s"Testing that ${hash.hashCode()} = -812840762")
assert(hash.hashCode == -812840762)
}
class HashServiceTest extends HashServiceBaseTest {
def imageHashTestCase(filePath: String): ImageHash = {
HashService.getImageHashes(ahashSetting, dhashSetting, phashSetting, filePath)
}
test("ImageHash Of Large, Medium, And Small Sample 1 Must Be Similar") {
val largeHash = imageHashTestCase(LargeSampleImage1)
val mediumHash = imageHashTestCase(MediumSampleImage1)
val smallHash = imageHashTestCase(SmallSampleImage1)
assert(HashService.areImageHashesSimilar(ahashSetting, dhashSetting, phashSetting, largeHash, mediumHash))
assert(HashService.areImageHashesSimilar(ahashSetting, dhashSetting, phashSetting, largeHash, smallHash))
assert(HashService.areImageHashesSimilar(ahashSetting, dhashSetting, phashSetting, mediumHash, smallHash))
}
test("Calculate ImageHash Large Sample Image 1") {
debug("Starting 'Calculate ImageHash Large Sample Image 1' test")
val hash = HashService.getImageHashes(ahashSetting, dhashSetting, phashSetting, LargeSampleImage1)
debug(s"Testing that ${hash.hashCode()} = -812844858")
assert(hash.hashCode == -812844858)
}
test("Calculate ImageHash Medium Sample Image 1") {
debug("Starting 'Calculate ImageHash Medium Sample Image 1' test")
val hash = HashService.getImageHashes(ahashSetting, dhashSetting, phashSetting, MediumSampleImage1)
debug(s"Testing that ${hash.hashCode()} = -812836666")
assert(hash.hashCode == -812836666)
}
test("Calculate ImageHash Small Sample Image 1") {
debug("Starting 'Calculate ImageHash Small Sample Image 1' test")
val hash = HashService.getImageHashes(ahashSetting, dhashSetting, phashSetting, SmallSampleImage1)
debug(s"Testing that ${hash.hashCode()} = -812840762")
assert(hash.hashCode == -812840762)
}
test("Benchmark getImageHashes") {
info("Benchmarking getImageHashes")
info("getImageHashes Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
time += getTime {
imageHashTestCase(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(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(SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
time.clear()
assert(true)
}
test("GetPrecisionSet - Empty Precision Set") {
val emptyPrecisionSet = HashService.getPrecisionSet(
new HashSetting("Test1", false, 8, 0, 0),
new HashSetting("Test2", false, 16, 0, 0),
new HashSetting("Test3", false, 32, 0, 0))
assert(emptyPrecisionSet.isEmpty)
}
test("GetPrecisionSet - Overlapping Precision Set") {
val precisionSet = HashService.getPrecisionSet(
new HashSetting("Test1", true, 8, 0, 0),
new HashSetting("Test2", true, 8, 0, 0),
new HashSetting("Test3", true, 16, 0, 0))
assert(precisionSet.nonEmpty)
assert(2 == precisionSet.size)
}
test("GetPrecisionMap - Empty Precision Set") {
val emptyPrecisionSet = HashService.getPrecisionSet(
new HashSetting("Test1", false, 8, 0, 0),
new HashSetting("Test2", false, 16, 0, 0),
new HashSetting("Test3", false, 32, 0, 0))
val grayImage = ImageUtil.convertToGray(ImageIO.read(new File(SmallSampleImage1)))
val emptyPrecisionMap = HashService.getPrecisionMap(emptyPrecisionSet, grayImage)
assert(emptyPrecisionMap.isEmpty)
}
test("GetPrecisionMap - Overlapping Precision Set") {
val precisionSet = HashService.getPrecisionSet(
new HashSetting("Test1", true, 8, 0, 0),
new HashSetting("Test2", true, 8, 0, 0),
new HashSetting("Test3", true, 16, 0, 0))
val grayImage = ImageUtil.convertToGray(ImageIO.read(new File(SmallSampleImage1)))
val precisionMap = HashService.getPrecisionMap(precisionSet, grayImage)
assert(precisionMap.nonEmpty)
assert(precisionMap.size == 2)
}
test("getHash - Empty Precision Set") {
val emptyPrecisionSet = HashService.getPrecisionSet(
new HashSetting("Test1", false, 8, 0, 0),
new HashSetting("Test2", false, 16, 0, 0),
new HashSetting("Test3", false, 32, 0, 0))
val grayImage = ImageUtil.convertToGray(ImageIO.read(new File(SmallSampleImage1)))
val emptyPrecisionMap = HashService.getPrecisionMap(emptyPrecisionSet, grayImage)
assertThrows[NoSuchElementException] {
val ahash: Long = getHash(ahashSetting, emptyPrecisionMap(ahashSetting.precision), AHash.getHash)
val dhash: Long = getHash(dhashSetting, emptyPrecisionMap(dhashSetting.precision), DHash.getHash)
val phash: Long = getHash(phashSetting, emptyPrecisionMap(phashSetting.precision), PHash.getHash)
}
}
} }

7
hash/src/test/scala/com/sothr/imagetools/hash/TestParams.scala

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

4
parent/pom.xml

@ -70,8 +70,10 @@
</pluginRepositories> </pluginRepositories>
<properties> <properties>
<imagetools.hash.version>0.1.1</imagetools.hash.version>
<!-- Image Tools Library Versions -->
<imagetools.hash.version>0.2.0</imagetools.hash.version>
<imagetools.engine.version>0.1.3</imagetools.engine.version> <imagetools.engine.version>0.1.3</imagetools.engine.version>
<!-- Other Dependency Versions -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>1.8</jdk.version> <jdk.version>1.8</jdk.version>
<scala.binary.version>2.12</scala.binary.version> <scala.binary.version>2.12</scala.binary.version>

Loading…
Cancel
Save