Browse Source

Extracted Hash into separate library for use in other applications

master
Drew Short 6 years ago
parent
commit
74bb5cdbe0
  1. 8
      engine/pom.xml
  2. 2
      engine/src/main/java/com/sothr/imagetools/engine/AppConfig.java
  3. 4
      engine/src/main/resources/application.conf
  4. 2
      engine/src/main/resources/hibernate/Image.hbm.xml
  5. 2
      engine/src/main/resources/hibernate/ImageHash.hbm.xml
  6. 10
      engine/src/main/scala/com/sothr/imagetools/engine/ConcurrentEngine.scala
  7. 210
      engine/src/main/scala/com/sothr/imagetools/engine/hash/HashService.scala
  8. 25
      engine/src/main/scala/com/sothr/imagetools/engine/image/Image.scala
  9. 147
      engine/src/main/scala/com/sothr/imagetools/engine/image/ImageService.scala
  10. 54
      engine/src/main/scala/com/sothr/imagetools/engine/util/PropertiesService.scala
  11. 8
      engine/src/main/scala/com/sothr/imagetools/engine/vo/ImageHashVO.scala
  12. 4
      engine/src/test/resources/application.conf
  13. 2
      engine/src/test/resources/hibernate/Image.hbm.xml
  14. 2
      engine/src/test/resources/hibernate/ImageHash.hbm.xml
  15. 121
      hash/pom.xml
  16. BIN
      hash/sample/sample_01_large.jpg
  17. BIN
      hash/sample/sample_01_medium.jpg
  18. BIN
      hash/sample/sample_01_small.jpg
  19. 186
      hash/src/main/scala/com/sothr/imagetools/hash/HashService.scala
  20. 8
      hash/src/main/scala/com/sothr/imagetools/hash/dto/HashSettingDTO.scala
  21. 46
      hash/src/main/scala/com/sothr/imagetools/hash/dto/ImageHashDTO.scala
  22. 8
      hash/src/main/scala/com/sothr/imagetools/hash/type/AHash.scala
  23. 8
      hash/src/main/scala/com/sothr/imagetools/hash/type/DHash.scala
  24. 6
      hash/src/main/scala/com/sothr/imagetools/hash/type/PHash.scala
  25. 2
      hash/src/main/scala/com/sothr/imagetools/hash/type/PerceptualHasher.scala
  26. 18
      hash/src/main/scala/com/sothr/imagetools/hash/util/HammingUtil.scala
  27. 117
      hash/src/main/scala/com/sothr/imagetools/hash/util/ImageUtil.scala
  28. 35
      hash/src/main/scala/com/sothr/imagetools/hash/util/TimingUtil.scala
  29. 9
      hash/src/test/scala/com/sothr/imagetools/hash/BaseTest.scala
  30. 149
      hash/src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala
  31. 7
      hash/src/test/scala/com/sothr/imagetools/hash/TestParams.scala
  32. 6
      parent/pom.xml
  33. 2
      pom.xml

8
engine/pom.xml

@ -21,6 +21,10 @@
</organization>
<dependencies>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>ImageTools-Hash</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@ -55,6 +59,10 @@
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
<dependency>
<groupId>com.jsuereth</groupId>
<artifactId>scala-arm_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>

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

@ -16,7 +16,7 @@ import org.slf4j.LoggerFactory;
import java.io.File;
public class AppConfig {
public class AppConfig {
// Logging defaults
private static final String LOGSETTINGSFILE = "./logback.xml";

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

@ -48,13 +48,13 @@ app {
//Default Thumbnail Settings
thumbnail {
//Directory where to store thumbnails
directory = "./cache/thumbnails/"
directory = "./.cache/thumbnails/"
//Size of the thumbnail to generate and store
size = 128
}
//Default Database Settings
database {
connectionURL = "jdbc:h2:./cache/imageTools"
connectionURL = "jdbc:h2:./.cache/imageTools"
inMemory = false
}
}

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

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

2
engine/src/main/resources/hibernate/ImageHash.hbm.xml

@ -3,7 +3,7 @@
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.sothr.imagetools.engine.dto.ImageHashDTO" table="ImageHash">
<class name="com.sothr.imagetools.engine.vo.ImageHashVO" table="ImageHash">
<meta attribute="class-description">
This class contains the image hashes
</meta>

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

@ -7,10 +7,10 @@ import akka.actor.{Actor, ActorLogging, ActorRef, PoisonPill, Props}
import akka.pattern.ask
import akka.routing.{Broadcast, RoundRobinPool, SmallestMailboxPool}
import akka.util.Timeout
import com.sothr.imagetools.engine.hash.HashService
import com.sothr.imagetools.hash.HashService
import com.sothr.imagetools.engine.image.{Image, ImageService, SimilarImages}
import com.sothr.imagetools.engine.util
import com.sothr.imagetools.engine.util.{PropertiesService, PropertyEnum}
import com.sothr.imagetools.hash.dto.ImageHashDTO
import scala.collection.mutable
import scala.concurrent.Await
@ -21,7 +21,7 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
implicit val timeout = Timeout(30, TimeUnit.SECONDS)
override def setSearchedListener(listenerRef: ActorRef) = {
this.searchedListener = listenerRef;
this.searchedListener = listenerRef
}
override def setProcessedListener(listenerRef: ActorRef) = {
@ -416,7 +416,9 @@ class ConcurrentEngineSimilarityActor extends Actor with ActorLogging {
val similarImages = new mutable.MutableList[Image]()
for (image <- command.images) {
if (!command.image1.equals(image)) {
if (HashService.areImageHashesSimilar(command.image1.hashes, image.hashes)) {
val image1Hashes = ImageService.convertToImageHashDTO(command.image1.hashes)
val imageHashes = ImageService.convertToImageHashDTO(image.hashes)
if (HashService.areImageHashesSimilar(PropertiesService.aHashSettings, PropertiesService.dHashSettings, PropertiesService.pHashSettings, image1Hashes, imageHashes)) {
similarImages += image
}
}

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

@ -1,210 +0,0 @@
package com.sothr.imagetools.engine.hash
import java.awt.image.BufferedImage
import java.io.{File, FileInputStream}
import javax.imageio.ImageIO
import com.sothr.imagetools.engine.dto.ImageHashDTO
import com.sothr.imagetools.engine.image.ImageService
import com.sothr.imagetools.engine.util.{Hamming, PropertiesService}
import grizzled.slf4j.Logging
import org.apache.commons.codec.digest.DigestUtils
import resource._
/**
* A service that exposes the ability to construct perceptive hashes from an
* image which can be used to find a perceptual difference between two or more
* images
*/
object HashService extends Logging {
def getImageHashes(imagePath: String): ImageHashDTO = {
//debug(s"Creating hashes for $imagePath")
getImageHashes(ImageIO.read(new File(imagePath)), imagePath)
}
def getImageHashes(image: BufferedImage, imagePath: String): ImageHashDTO = {
//debug("Creating hashes for an image")
var ahash: Long = 0L
var dhash: Long = 0L
var phash: Long = 0L
val sha1: String = getSHA1(imagePath)
//Get Image Data
val grayImage = ImageService.convertToGray(image)
if (PropertiesService.useAhash) {
ahash = getAhash(grayImage, alreadyGray = true)
}
if (PropertiesService.useDhash) {
dhash = getDhash(grayImage, alreadyGray = true)
}
if (PropertiesService.usePhash) {
phash = getPhash(grayImage, alreadyGray = true)
}
val hashes = new ImageHashDTO(ahash, dhash, phash, sha1)
debug(s"Generated hashes: $hashes")
hashes
}
def getAhash(image: BufferedImage, alreadyGray: Boolean = false): Long = {
//debug("Started generating an AHash")
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
grayImage = ImageService.convertToGray(image)
}
val resizedImage = ImageService.resize(grayImage, PropertiesService.aHashPrecision, forced = true)
val imageData = ImageService.getImageData(resizedImage)
AHash.getHash(imageData)
}
def getDhash(image: BufferedImage, alreadyGray: Boolean = false): Long = {
//debug("Started generating an DHash")
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
grayImage = ImageService.convertToGray(image)
}
val resizedImage = ImageService.resize(grayImage, PropertiesService.dHashPrecision, forced = true)
val imageData = ImageService.getImageData(resizedImage)
DHash.getHash(imageData)
}
def getPhash(image: BufferedImage, alreadyGray: Boolean = false): Long = {
//debug("Started generating an PHash")
var grayImage: BufferedImage = null
if (alreadyGray) {
grayImage = image
} else {
grayImage = ImageService.convertToGray(image)
}
val resizedImage = ImageService.resize(grayImage, PropertiesService.pHashPrecision, forced = true)
val imageData = ImageService.getImageData(resizedImage)
PHash.getHash(imageData)
}
def getMD5(filePath: String): String = {
managed(new FileInputStream(filePath)) acquireAndGet {
input => DigestUtils.md5Hex(input)
}
}
def getSHA1(filePath: String): String = {
managed(new FileInputStream(filePath)) acquireAndGet {
input => DigestUtils.sha1Hex(input)
}
}
def areAhashSimilar(ahash1: Long, ahash2: Long): Boolean = {
val tolerence = PropertiesService.aHashTolerance
val distance = Hamming.getDistance(ahash1, ahash2)
//debug(s"hash1: $ahash1 hash2: $ahash2 tolerence: $tolerence hamming distance: $distance")
if (distance <= tolerence) true else false
}
def areDhashSimilar(dhash1: Long, dhash2: Long): Boolean = {
val tolerence = PropertiesService.dHashTolerance
val distance = Hamming.getDistance(dhash1, dhash2)
//debug(s"hash1: $dhash1 hash2: $dhash2 tolerence: $tolerence hamming distance: $distance")
if (distance <= tolerence) true else false
}
def arePhashSimilar(phash1: Long, phash2: Long): Boolean = {
val tolerence = PropertiesService.pHashTolerance
val distance = Hamming.getDistance(phash1, phash2)
//debug(s"hash1: $phash1 hash2: $phash2 tolerence: $tolerence hamming distance: $distance")
if (distance <= tolerence) true else false
}
def areImageHashesSimilar(imageHash1: ImageHashDTO, imageHash2: ImageHashDTO): Boolean = {
val weightedHammingMean = getWeightedHashSimilarity(imageHash1, imageHash2)
val weightedToleranceMean = getWeightedHashTolerence
if (weightedHammingMean <= weightedToleranceMean) true else false
}
def getWeightedHashSimilarity(imageHash1: ImageHashDTO, imageHash2: ImageHashDTO): Float = {
//ahash
val aHashTolerance = PropertiesService.aHashTolerance
val aHashWeight = PropertiesService.aHashWeight
val useAhash = PropertiesService.useAhash
//dhash
val dHashTolerance = PropertiesService.dHashTolerance
val dHashWeight = PropertiesService.dHashWeight
val useDhash = PropertiesService.useAhash
//phash
val pHashTolerance = PropertiesService.pHashTolerance
val pHashWeight = PropertiesService.pHashWeight
val usePhash = PropertiesService.useAhash
//calculate weighted values
var weightedHammingTotal: Float = 0
var methodsTotal = 0
if (useAhash) {
val hamming = Hamming.getDistance(imageHash1.ahash, imageHash2.ahash)
weightedHammingTotal += hamming * aHashWeight
//debug(s"hash1: ${imageHash1.ahash} hash2: ${imageHash1.ahash} tolerence: $aHashTolerance hamming distance: $hamming weight: $aHashWeight")
methodsTotal += 1
}
if (useDhash) {
val hamming = Hamming.getDistance(imageHash1.dhash, imageHash2.dhash)
weightedHammingTotal += hamming * dHashWeight
//debug(s"hash1: ${imageHash1.dhash} hash2: ${imageHash1.dhash} tolerence: $dHashTolerance hamming distance: $hamming weight: $dHashWeight")
methodsTotal += 1
}
if (usePhash) {
val hamming = Hamming.getDistance(imageHash1.phash, imageHash2.phash)
weightedHammingTotal += hamming * pHashWeight
//debug(s"hash1: ${imageHash1.phash} hash2: ${imageHash1.phash} tolerence: $pHashTolerance hamming distance: $hamming weight: $pHashWeight")
methodsTotal += 1
}
val weightedHammingMean = weightedHammingTotal / methodsTotal
//debug(s"Calculated Weighted Hamming Mean: $weightedHammingMean")
weightedHammingMean
}
def getWeightedHashTolerence: Float = {
//ahash
val aHashTolerance = PropertiesService.aHashTolerance
val aHashWeight = PropertiesService.aHashWeight
val useAhash = PropertiesService.useAhash
//dhash
val dHashTolerance = PropertiesService.dHashTolerance
val dHashWeight = PropertiesService.dHashWeight
val useDhash = PropertiesService.useAhash
//phash
val pHashTolerance = PropertiesService.pHashTolerance
val pHashWeight = PropertiesService.pHashWeight
val usePhash = PropertiesService.useAhash
//calculate weighted values
var weightedToleranceTotal: Float = 0
var methodsTotal = 0
if (useAhash) {
weightedToleranceTotal += aHashTolerance * aHashWeight
//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")
methodsTotal += 1
}
if (usePhash) {
weightedToleranceTotal += pHashTolerance * pHashWeight
//debug(s"Phash Tolerance: $pHashTolerance Current Weighted Tolerance: $weightedToleranceTotal")
methodsTotal += 1
}
val weightedTolerance = weightedToleranceTotal / methodsTotal
//debug(s"Calculated Weighted Tolerance: $weightedTolerance")
weightedTolerance
}
}

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

@ -2,20 +2,21 @@ package com.sothr.imagetools.engine.image
import javax.persistence._
import com.sothr.imagetools.engine.dto.ImageHashDTO
import com.sothr.imagetools.engine.hash.HashService
import com.sothr.imagetools.engine.util.PropertiesService
import com.sothr.imagetools.engine.vo.ImageHashVO
import com.sothr.imagetools.hash.HashService
import grizzled.slf4j.Logging
@Entity
@Table(name = "Image")
class Image(val image: String, val thumbnail: String, val size: (Int, Int), val imageHashes: ImageHashDTO = null) extends Serializable with Logging {
class Image(val image: String, val thumbnail: String, val size: (Int, Int), val imageHashes: ImageHashVO = null) extends Serializable with Logging {
@Id
var imagePath: String = image
var thumbnailPath: String = thumbnail
var width: Int = size._1
var height: Int = size._2
var hashes: ImageHashDTO = imageHashes
var hashes: ImageHashVO = imageHashes
@transient
var imageSize: (Int, Int) = {
new Tuple2(width, height)
@ -44,9 +45,9 @@ class Image(val image: String, val thumbnail: String, val size: (Int, Int), val
height = size
}
def getHashes: ImageHashDTO = hashes
def getHashes: ImageHashVO = hashes
def setHashes(newHashes: ImageHashDTO) = {
def setHashes(newHashes: ImageHashVO) = {
hashes = newHashes
}
@ -65,11 +66,19 @@ class Image(val image: String, val thumbnail: String, val size: (Int, Int), val
def isSimilarTo(otherImage: Image): Boolean = {
//debug(s"Checking $imagePath for similarities with ${otherImage.imagePath}")
HashService.areImageHashesSimilar(this.hashes, otherImage.hashes)
HashService.areImageHashesSimilar(PropertiesService.aHashSettings,
PropertiesService.dHashSettings,
PropertiesService.pHashSettings,
ImageService.convertToImageHashDTO(this.hashes),
ImageService.convertToImageHashDTO(otherImage.hashes))
}
def getSimilarity(otherImage: Image): Float = {
HashService.getWeightedHashSimilarity(this.hashes, otherImage.hashes)
HashService.getWeightedHashSimilarity(PropertiesService.aHashSettings,
PropertiesService.dHashSettings,
PropertiesService.pHashSettings,
ImageService.convertToImageHashDTO(this.hashes),
ImageService.convertToImageHashDTO(otherImage.hashes))
}
/*def getSimilar(otherImages:Traversable[Image]):Traversable[Image] = {

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

@ -1,15 +1,17 @@
package com.sothr.imagetools.engine.image
import java.awt.image.{BufferedImage, ColorConvertOp, DataBufferByte}
import java.awt.image.BufferedImage
import java.io.{File, IOException}
import javax.imageio.ImageIO
import com.sothr.imagetools.engine.AppConfig
import com.sothr.imagetools.engine.dao.ImageDAO
import com.sothr.imagetools.engine.hash.HashService
import com.sothr.imagetools.engine.util.{PropertiesService, PropertyEnum}
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.util.ImageUtil
import grizzled.slf4j.Logging
import net.coobird.thumbnailator.Thumbnails
import net.sf.ehcache.Element
import org.hibernate.HibernateException
@ -27,13 +29,19 @@ object ImageService extends Logging {
} else {
debug(s"Processing image: ${file.getAbsolutePath}")
val bufferedImage = ImageIO.read(file)
val hashes = HashService.getImageHashes(bufferedImage, file.getAbsolutePath)
val hashes = HashService.getImageHashes(
PropertiesService.aHashSettings,
PropertiesService.dHashSettings,
PropertiesService.pHashSettings,
bufferedImage,
file.getAbsolutePath)
var thumbnailPath = lookupThumbnailPath(hashes.getFileHash)
if (thumbnailPath == null) thumbnailPath = getThumbnail(bufferedImage, hashes.getFileHash)
val imageSize = {
(bufferedImage.getWidth, bufferedImage.getHeight)
}
val image = new Image(file.getAbsolutePath, thumbnailPath, imageSize, hashes)
val image = new Image(file.getAbsolutePath, thumbnailPath, imageSize, ImageService.convertToImageHashVO(hashes))
debug(s"Created image: $image")
return saveImage(image)
}
@ -44,6 +52,14 @@ object ImageService extends Logging {
null
}
def convertToImageHashDTO(imageHashVO: ImageHashVO): ImageHashDTO = {
new ImageHashDTO(imageHashVO.ahash, imageHashVO.dhash, imageHashVO.phash, imageHashVO.fileHash)
}
def convertToImageHashVO(imageHashDTO: ImageHashDTO): ImageHashVO = {
new ImageHashVO(imageHashDTO.ahash, imageHashDTO.dhash, imageHashDTO.phash, imageHashDTO.fileHash)
}
private def lookupImage(file: File): Image = {
var image: Image = null
var found = false
@ -108,7 +124,7 @@ object ImageService extends Logging {
def getThumbnail(image: BufferedImage, md5: String): String = {
//create thumbnail
val thumb = resize(image, PropertiesService.get(PropertyEnum.ThumbnailSize.toString).toInt, forced = false)
val thumb = ImageUtil.resize(image, PropertiesService.get(PropertyEnum.ThumbnailSize.toString).toInt, forced = false)
//calculate path
val path = calculateThumbPath(md5)
// save thumbnail to path
@ -122,15 +138,6 @@ object ImageService extends Logging {
path
}
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 deleteImage(image: Image) = {
debug(s"Attempting to delete all traces of image: ${image.getImagePath}")
try {
@ -146,114 +153,4 @@ object ImageService extends Logging {
case he: HibernateException => error(s"Unable to delete file: ${image.getImagePath} due to a hibernate exception", he)
}
}
/**
* Get the raw data for an image
*/
def getImageData(image: BufferedImage): Array[Array[Int]] = {
convertTo2DWithoutUsingGetRGB(image)
}
/**
* Convert a buffered image into a 2d pixel data array
*
* @param image image to convert without using RGB
* @return
*/
private def convertTo2DWithoutUsingGetRGB(image: BufferedImage): Array[Array[Int]] = {
val pixels = image.getRaster.getDataBuffer.asInstanceOf[DataBufferByte].getData
val numPixels = pixels.length
val width = image.getWidth
val height = image.getHeight
val isSingleChannel = if (numPixels == (width * height)) true else false
val 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
}
/**
* Quickly convert an image to grayscale
*
* @param image image to convert to greyscale
* @return
*/
def convertToGray(image: BufferedImage): BufferedImage = {
//debug("Converting an image to grayscale")
val grayImage = new BufferedImage(image.getWidth, image.getHeight, BufferedImage.TYPE_BYTE_GRAY)
//create a color conversion operation
val op = new ColorConvertOp(
image.getColorModel.getColorSpace,
grayImage.getColorModel.getColorSpace, null)
//convert the image to grey
val result = op.filter(image, grayImage)
//val g = image.getGraphics
//g.drawImage(image,0,0,null)
//g.dispose()
result
}
}

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

@ -3,6 +3,7 @@ package com.sothr.imagetools.engine.util
import java.io.{File, FileOutputStream, PrintStream}
import java.util.Properties
import com.sothr.imagetools.hash.dto.HashSettingDTO
import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions}
import grizzled.slf4j.Logging
@ -19,21 +20,9 @@ object PropertiesService extends Logging {
private val configRenderOptions = ConfigRenderOptions.concise().setFormatted(true)
//specific highly used properties
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
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
@ -58,21 +47,26 @@ object PropertiesService extends Logging {
//load special properties
TimingEnabled = get(PropertyEnum.Timed.toString).toBoolean
//ahash
aHashPrecision = get(PropertyEnum.AhashPrecision.toString).toInt
aHashTolerance = get(PropertyEnum.AhashTolerance.toString).toInt
aHashWeight = get(PropertyEnum.AhashWeight.toString).toFloat
useAhash = get(PropertyEnum.UseAhash.toString).toBoolean
//dhash
dHashPrecision = get(PropertyEnum.DhashPrecision.toString).toInt
dHashTolerance = get(PropertyEnum.DhashTolerance.toString).toInt
dHashWeight = get(PropertyEnum.DhashWeight.toString).toFloat
useDhash = get(PropertyEnum.UseDhash.toString).toBoolean
//phash
pHashPrecision = get(PropertyEnum.PhashPrecision.toString).toInt
pHashTolerance = get(PropertyEnum.PhashTolerance.toString).toInt
pHashWeight = get(PropertyEnum.PhashWeight.toString).toFloat
usePhash = get(PropertyEnum.UsePhash.toString).toBoolean
aHashSettings = new HashSettingDTO(
get(PropertyEnum.UseAhash.toString).toBoolean,
get(PropertyEnum.AhashPrecision.toString).toInt,
get(PropertyEnum.AhashTolerance.toString).toInt,
get(PropertyEnum.AhashWeight.toString).toFloat
)
dHashSettings = new HashSettingDTO(
get(PropertyEnum.UseDhash.toString).toBoolean,
get(PropertyEnum.DhashPrecision.toString).toInt,
get(PropertyEnum.DhashTolerance.toString).toInt,
get(PropertyEnum.DhashWeight.toString).toFloat
)
pHashSettings = new HashSettingDTO(
get(PropertyEnum.UsePhash.toString).toBoolean,
get(PropertyEnum.PhashPrecision.toString).toInt,
get(PropertyEnum.PhashTolerance.toString).toInt,
get(PropertyEnum.PhashWeight.toString).toFloat
)
info("Loaded Special Properties")
}

8
engine/src/main/scala/com/sothr/imagetools/engine/dto/ImageHashDTO.scala → engine/src/main/scala/com/sothr/imagetools/engine/vo/ImageHashVO.scala

@ -1,4 +1,4 @@
package com.sothr.imagetools.engine.dto
package com.sothr.imagetools.engine.vo
import javax.persistence._
@ -6,7 +6,7 @@ import grizzled.slf4j.Logging
@Entity
@Table(name = "ImageHash")
class ImageHashDTO(var ahash: Long, var dhash: Long, var phash: Long, var fileHash: String) extends Serializable with Logging {
class ImageHashVO(var ahash: Long, var dhash: Long, var phash: Long, var fileHash: String) extends Serializable with Logging {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@ -44,8 +44,8 @@ class ImageHashDTO(var ahash: Long, var dhash: Long, var phash: Long, var fileHa
fileHash = hash
}
def cloneHashes: ImageHashDTO = {
new ImageHashDTO(ahash, dhash, phash, fileHash)
def cloneHashes: ImageHashVO = {
new ImageHashVO(ahash, dhash, phash, fileHash)
}
override def hashCode(): Int = {

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

@ -48,13 +48,13 @@ app {
//Default Thumbnail Settings
thumbnail {
//Directory where to store thumbnails
directory = ".cache/thumbnails/"
directory = "./.test/.cache/thumbnails/"
//Size of the thumbnail to generate and store
size = 128
}
//Default Database Settings
database {
connectionURL = "jdbc:h2:.cache/imageTools"
connectionURL = "jdbc:h2:./.test/.cache/imageTools"
inMemory = false
}
}

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

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

2
engine/src/test/resources/hibernate/ImageHash.hbm.xml

@ -3,7 +3,7 @@
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.sothr.imagetools.engine.dto.ImageHashDTO" table="ImageHash">
<class name="com.sothr.imagetools.engine.vo.ImageHashVO" table="ImageHash">
<meta attribute="class-description">
This class contains the image hashes
</meta>

121
hash/pom.xml

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.sothr.imagetools</groupId>
<artifactId>Parent</artifactId>
<version>1.0.0</version>
<relativePath>../parent</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ImageTools-Hash</artifactId>
<version>0.1.0</version>
<packaging>jar</packaging>
<name>ImageTools-Hash</name>
<description>An image collection management utility</description>
<url>http://imagetools.sothr.com</url>
<organization>
<name>Sothr Software</name>
</organization>
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
<dependency>
<groupId>com.jsuereth</groupId>
<artifactId>scala-arm_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.jtransforms</groupId>
<artifactId>jtransforms</artifactId>
</dependency>
<!-- TEST -->
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- enable surefire for java tests-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7</version>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
<!-- enable scalatest for scala tests-->
<plugin>
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
<version>1.0-RC2</version>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
<junitxml>.</junitxml>
<filereports>WDF TestSuite.txt</filereports>
<argLine>-Xmx128m</argLine>
</configuration>
<executions>
<execution>
<id>test</id>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Packaging Configuration -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
<outputDirectory>
${project.build.directory}/release
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

BIN
hash/sample/sample_01_large.jpg

After

Width: 3648  |  Height: 2736  |  Size: 5.0 MiB

BIN
hash/sample/sample_01_medium.jpg

After

Width: 1824  |  Height: 1368  |  Size: 1.5 MiB

BIN
hash/sample/sample_01_small.jpg

After

Width: 912  |  Height: 684  |  Size: 519 KiB

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

@ -0,0 +1,186 @@
package com.sothr.imagetools.hash
import java.awt.image.BufferedImage
import java.io.{File, FileInputStream}
import javax.imageio.ImageIO
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 grizzled.slf4j.Logging
import org.apache.commons.codec.digest.DigestUtils
import resource.managed
/**
* A service that exposes the ability to construct perceptive hashes from an
* image which can be used to find a perceptual difference between two or more
* images
*/
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
}
}

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

@ -0,0 +1,8 @@
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

@ -0,0 +1,46 @@
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"
}
}

8
engine/src/main/scala/com/sothr/imagetools/engine/hash/AHash.scala → hash/src/main/scala/com/sothr/imagetools/hash/type/AHash.scala

@ -1,4 +1,4 @@
package com.sothr.imagetools.engine.hash
package com.sothr.imagetools.hash.`type`
import grizzled.slf4j.Logging
@ -10,10 +10,10 @@ import grizzled.slf4j.Logging
*/
object AHash extends PerceptualHasher with Logging {
def getHash(imageData: Array[Array[Int]]): Long = {
//debug("Generating AHash")
trace("Generating AHash")
val width = imageData.length
val height = imageData(0).length
//debug(s"Image data size: ${width}x${height}")
trace(s"Image data size: ${width}x$height")
//calculate average pixel
var total = 0
@ -45,7 +45,7 @@ object AHash extends PerceptualHasher with Logging {
}
}
}
//debug(s"Computed AHash: $hash from ${width * height} pixels")
debug(s"Computed AHash: $hash from ${width * height} pixels")
hash
}
}

8
engine/src/main/scala/com/sothr/imagetools/engine/hash/DHash.scala → hash/src/main/scala/com/sothr/imagetools/hash/type/DHash.scala

@ -1,4 +1,4 @@
package com.sothr.imagetools.engine.hash
package com.sothr.imagetools.hash.`type`
import grizzled.slf4j.Logging
@ -9,10 +9,10 @@ import grizzled.slf4j.Logging
*/
object DHash extends PerceptualHasher with Logging {
def getHash(imageData: Array[Array[Int]]): Long = {
//debug("Generating DHash")
trace("Generating DHash")
val width = imageData.length
val height = imageData(0).length
//debug(s"Image data size: ${width}x${height}")
trace(s"Image data size: ${width}x$height")
//calculate dhash
var hash = 0L
@ -51,7 +51,7 @@ object DHash extends PerceptualHasher with Logging {
}
}
}
//debug(s"Computed DHash: $hash from ${width * height} pixels")
debug(s"Computed DHash: $hash from ${width * height} pixels")
hash
}
}

6
engine/src/main/scala/com/sothr/imagetools/engine/hash/PHash.scala → hash/src/main/scala/com/sothr/imagetools/hash/type/PHash.scala

@ -1,4 +1,4 @@
package com.sothr.imagetools.engine.hash
package com.sothr.imagetools.hash.`type`
import edu.emory.mathcs.jtransforms.dct.FloatDCT_2D
import grizzled.slf4j.Logging
@ -14,7 +14,7 @@ object PHash extends PerceptualHasher with Logging {
//convert the imageData into a FloatArray
val width = imageData.length
val height = imageData(0).length
//debug(s"Starting with image of ${height}x${width} for PHash")
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) {
@ -66,7 +66,7 @@ object PHash extends PerceptualHasher with Logging {
}
}
}
//debug(s"Computed PHash: $hash from ${dctDataWidth * dctDataHeight} pixels")
debug(s"Computed PHash: $hash from ${dctDataWidth * dctDataHeight} pixels")
hash
}
}

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

@ -1,4 +1,4 @@
package com.sothr.imagetools.engine.hash
package com.sothr.imagetools.hash.`type`
/**
* Interface for perceptual hashing

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

@ -0,0 +1,18 @@
package com.sothr.imagetools.hash.util
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')
}
}

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

@ -0,0 +1,117 @@
package com.sothr.imagetools.hash.util
import java.awt.image.{BufferedImage, ColorConvertOp, DataBufferByte}
import grizzled.slf4j.Logging
import net.coobird.thumbnailator.Thumbnails
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)
val op = new ColorConvertOp(
image.getColorModel.getColorSpace,
grayImage.getColorModel.getColorSpace,
null
)
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
}
}
/**
* 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
}
}

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

@ -0,0 +1,35 @@
package com.sothr.imagetools.hash.util
import grizzled.slf4j.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 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
}
}

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

@ -0,0 +1,9 @@
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 {
}

149
engine/src/test/scala/com/sothr/imagetools/engine/hash/HashServiceTest.scala → hash/src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala

@ -1,35 +1,37 @@
package com.sothr.imagetools.engine.hash
package com.sothr.imagetools.hash
import java.io.File
import javax.imageio.ImageIO
import com.sothr.imagetools.engine.dto.ImageHashDTO
import com.sothr.imagetools.engine.{AppConfig, BaseTest, TestParams}
import net.sf.ehcache.Element
import com.sothr.imagetools.hash.`type`.DHash
import com.sothr.imagetools.hash.dto.{HashSettingDTO, ImageHashDTO}
import scala.collection.mutable
/**
* Test the Hash service and make sure it is consistent
*
* Created by dev on 1/23/14.
*/
* Test the Hash service and make sure it is consistent
*
* 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(image)
HashService.getDhash(dhashSetting, image)
}
test("Benchmark DHash") {
info("Benchmarking DHash")
info("DHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
dhashTestCase(TestParams.LargeSampleImage1)
}
@ -38,7 +40,7 @@ class HashServiceTest extends BaseTest {
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("DHash Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
dhashTestCase(TestParams.MediumSampleImage1)
}
@ -47,7 +49,7 @@ class HashServiceTest extends BaseTest {
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("DHash Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
dhashTestCase(TestParams.SmallSampleImage1)
}
@ -79,7 +81,7 @@ class HashServiceTest extends BaseTest {
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(image)
val hash = HashService.getDhash(dhashSetting, image)
debug(s"Testing that $hash = 4004374827879799635L")
assert(hash == 4004374827879799635L)
}
@ -90,7 +92,7 @@ class HashServiceTest extends BaseTest {
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(image)
val hash = HashService.getDhash(dhashSetting, image)
debug(s"Testing that $hash = 4004374827879799635L")
assert(hash == 4004374827879799635L)
}
@ -101,7 +103,7 @@ class HashServiceTest extends BaseTest {
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(image)
val hash = HashService.getDhash(dhashSetting, image)
debug(s"Testing that $hash = 4004383623972821843L")
assert(hash == 4004383623972821843L)
}
@ -110,22 +112,22 @@ class HashServiceTest extends BaseTest {
val largeHash = dhashTestCase(TestParams.LargeSampleImage1)
val mediumHash = dhashTestCase(TestParams.MediumSampleImage1)
val smallHash = dhashTestCase(TestParams.SmallSampleImage1)
assert(HashService.areDhashSimilar(largeHash, mediumHash))
assert(HashService.areDhashSimilar(largeHash, smallHash))
assert(HashService.areDhashSimilar(mediumHash, smallHash))
assert(HashService.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(image)
HashService.getAhash(ahashSetting, image)
}
test("Benchmark AHash") {
info("Benchmarking AHash")
info("AHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
ahashTestCase(TestParams.LargeSampleImage1)
}
@ -134,7 +136,7 @@ class HashServiceTest extends BaseTest {
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("AHash Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
ahashTestCase(TestParams.MediumSampleImage1)
}
@ -143,7 +145,7 @@ class HashServiceTest extends BaseTest {
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("AHash Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
ahashTestCase(TestParams.SmallSampleImage1)
}
@ -160,7 +162,7 @@ class HashServiceTest extends BaseTest {
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(image)
val hash = HashService.getAhash(ahashSetting, image)
debug(s"Testing that $hash = 36070299219713907L")
assert(hash == 36070299219713907L)
}
@ -171,7 +173,7 @@ class HashServiceTest extends BaseTest {
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(image)
val hash = HashService.getAhash(ahashSetting, image)
debug(s"Testing that $hash = 36070299219713907L")
assert(hash == 36070299219713907L)
}
@ -182,7 +184,7 @@ class HashServiceTest extends BaseTest {
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(image)
val hash = HashService.getAhash(ahashSetting, image)
debug(s"Testing that $hash = 36070299219713907L")
assert(hash == 36070299219713907L)
}
@ -191,22 +193,22 @@ class HashServiceTest extends BaseTest {
val largeHash = ahashTestCase(TestParams.LargeSampleImage1)
val mediumHash = ahashTestCase(TestParams.MediumSampleImage1)
val smallHash = ahashTestCase(TestParams.SmallSampleImage1)
assert(HashService.areAhashSimilar(largeHash, mediumHash))
assert(HashService.areAhashSimilar(largeHash, smallHash))
assert(HashService.areAhashSimilar(mediumHash, smallHash))
assert(HashService.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(image)
HashService.getPhash(phashSetting, image)
}
test("Benchmark PHash") {
info("Benchmarking PHash")
info("PHash Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
phashTestCase(TestParams.LargeSampleImage1)
}
@ -215,7 +217,7 @@ class HashServiceTest extends BaseTest {
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("PHash Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
phashTestCase(TestParams.MediumSampleImage1)
}
@ -224,7 +226,7 @@ class HashServiceTest extends BaseTest {
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("PHash Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
phashTestCase(TestParams.SmallSampleImage1)
}
@ -241,7 +243,7 @@ class HashServiceTest extends BaseTest {
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(image)
val hash = HashService.getPhash(phashSetting, image)
debug(s"Testing that $hash = -9154554603604154117L")
assert(hash == -9154554603604154117L)
}
@ -252,7 +254,7 @@ class HashServiceTest extends BaseTest {
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(image)
val hash = HashService.getPhash(phashSetting, image)
debug(s"Testing that $hash = -9154589787976242949L")
assert(hash == -9154589787976242949L)
}
@ -263,7 +265,7 @@ class HashServiceTest extends BaseTest {
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(image)
val hash = HashService.getPhash(phashSetting, image)
debug(s"Testing that $hash = -9154589787976242949L")
assert(hash == -9154589787976242949L)
}
@ -272,9 +274,9 @@ class HashServiceTest extends BaseTest {
val largeHash = phashTestCase(TestParams.LargeSampleImage1)
val mediumHash = phashTestCase(TestParams.MediumSampleImage1)
val smallHash = phashTestCase(TestParams.SmallSampleImage1)
assert(HashService.arePhashSimilar(largeHash, mediumHash))
assert(HashService.arePhashSimilar(largeHash, smallHash))
assert(HashService.arePhashSimilar(mediumHash, smallHash))
assert(HashService.areHashSimilar(phashSetting, largeHash, mediumHash))
assert(HashService.areHashSimilar(phashSetting, largeHash, smallHash))
assert(HashService.areHashSimilar(phashSetting, mediumHash, smallHash))
}
def md5TestCase(filePath: String): String = {
@ -285,7 +287,7 @@ class HashServiceTest extends BaseTest {
info("Benchmarking MD5")
info("MD5 Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
md5TestCase(TestParams.LargeSampleImage1)
}
@ -294,7 +296,7 @@ class HashServiceTest extends BaseTest {
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("MD5 Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
md5TestCase(TestParams.MediumSampleImage1)
}
@ -303,7 +305,7 @@ class HashServiceTest extends BaseTest {
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("MD5 Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
md5TestCase(TestParams.SmallSampleImage1)
}
@ -343,7 +345,7 @@ class HashServiceTest extends BaseTest {
info("Benchmarking SHA1")
info("SHA1 Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
sha1TestCase(TestParams.LargeSampleImage1)
}
@ -352,7 +354,7 @@ class HashServiceTest extends BaseTest {
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("SHA1 Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
sha1TestCase(TestParams.MediumSampleImage1)
}
@ -361,7 +363,7 @@ class HashServiceTest extends BaseTest {
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("SHA1 Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
for (_ <- 0 until benchmarkRuns) {
time += getTime {
sha1TestCase(TestParams.SmallSampleImage1)
}
@ -393,53 +395,8 @@ class HashServiceTest extends BaseTest {
assert(hash == "1a91d2b5327f0aad258419f76b87d4c0bc343443")
}
def imageHashTestWithCacheCase(filePath: String): ImageHashDTO = {
val cache = AppConfig.cacheManager.getCache("images")
var result: ImageHashDTO = null
if (cache.get(filePath) != null) {
result = cache.get(filePath).getObjectValue.asInstanceOf[ImageHashDTO]
} else {
result = imageHashTestCase(filePath)
cache.put(new Element(filePath, result))
}
result
}
def imageHashTestCase(filePath: String): ImageHashDTO = {
HashService.getImageHashes(filePath)
}
test("Benchmark getImageHashes with cache") {
info("Benchmarking getImageHashes with cache")
info("getImageHashes with cache Large Image 3684x2736")
val time = new mutable.MutableList[Long]()
for (runNum <- 0 until benchmarkRuns) {
time += getTime {
imageHashTestWithCacheCase(TestParams.LargeSampleImage1)
}
}
val largeMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for large was: $largeMean ms")
time.clear()
info("getImageHashes with cache Medium Image 1824x1368")
for (runNum <- 0 until benchmarkRuns) {
time += getTime {
imageHashTestWithCacheCase(TestParams.MediumSampleImage1)
}
}
val mediumMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for medium was: $mediumMean ms")
time.clear()
info("getImageHashes with cache Small Image 912x684")
for (runNum <- 0 until benchmarkRuns) {
time += getTime {
imageHashTestWithCacheCase(TestParams.SmallSampleImage1)
}
}
val smallMean = getMean(time.toArray[Long])
info(s"The mean time of ${time.size} tests for small was: $smallMean ms")
time.clear()
assert(true)
HashService.getImageHashes(ahashSetting, dhashSetting, phashSetting, filePath)
}
test("Benchmark getImageHashes") {
@ -479,28 +436,28 @@ class HashServiceTest extends BaseTest {
val largeHash = imageHashTestCase(TestParams.LargeSampleImage1)
val mediumHash = imageHashTestCase(TestParams.MediumSampleImage1)
val smallHash = imageHashTestCase(TestParams.SmallSampleImage1)
assert(HashService.areImageHashesSimilar(largeHash, mediumHash))
assert(HashService.areImageHashesSimilar(largeHash, smallHash))
assert(HashService.areImageHashesSimilar(mediumHash, smallHash))
assert(HashService.areImageHashesSimilar(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(TestParams.LargeSampleImage1)
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(TestParams.MediumSampleImage1)
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(TestParams.SmallSampleImage1)
val hash = HashService.getImageHashes(ahashSetting, dhashSetting, phashSetting, TestParams.SmallSampleImage1)
debug(s"Testing that ${hash.hashCode()} = -812840762")
assert(hash.hashCode == -812840762)
}

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

@ -0,0 +1,7 @@
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"
}

6
parent/pom.xml

@ -59,6 +59,7 @@
</pluginRepositories>
<properties>
<imagetools.hash.version>0.1.0</imagetools.hash.version>
<imagetools.engine.version>0.1.2</imagetools.engine.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>1.8</jdk.version>
@ -87,6 +88,11 @@
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>ImageTools-Hash</artifactId>
<version>${imagetools.hash.version}</version>
</dependency>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>ImageTools-Engine</artifactId>

2
pom.xml

@ -16,9 +16,9 @@
<modules>
<module>parent</module>
<module>hash</module>
<module>engine</module>
<module>cli</module>
<!--<module>daemon</module>-->
<module>gui</module>
</modules>

Loading…
Cancel
Save