diff --git a/pom.xml b/pom.xml index e314922..ce5abd5 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,11 @@ thumbnailator [0.4, 0.5) + + net.sourceforge.jtransforms + jtransforms + 2.4.0 + diff --git a/src/main/resources/default.properties b/src/main/resources/default.properties index 1888b7c..341e43b 100644 --- a/src/main/resources/default.properties +++ b/src/main/resources/default.properties @@ -24,7 +24,7 @@ image.dhash.tolerence=8 #set to false if hashing images is taking too long image.phash.use=true image.phash.weight=1.0 -image.phash.precision=16 +image.phash.precision=32 image.phash.tolerence=8 #Default Thumbnail Settings diff --git a/src/main/scala/com/sothr/imagetools/hash/PHash.scala b/src/main/scala/com/sothr/imagetools/hash/PHash.scala index b00435c..7a697d2 100644 --- a/src/main/scala/com/sothr/imagetools/hash/PHash.scala +++ b/src/main/scala/com/sothr/imagetools/hash/PHash.scala @@ -1,10 +1,63 @@ package com.sothr.imagetools.hash +import edu.emory.mathcs.jtransforms.dct.FloatDCT_2D +import grizzled.slf4j.Logging + /** * Created by dev on 1/22/14. */ -object PHash extends PerceptualHasher { +object PHash extends PerceptualHasher with Logging { def getHash(imageData: Array[Array[Int]]): Long = { - return 0L + //convert the imageData into a FloatArray + val width = imageData.length + val height = imageData(0).length + + 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) + + //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) + + //calculate the hash + var hash = 0L + for (row <- 0 until dctDataHeight by 2) { + //process each column + for (col <- 0 until dctDataWidth by 1) { + hash <<= 1 + val pixel = imageDataFloat(row)(col) + //If the current pixel is at or above the mean, store it as a one, else store it as a zero + if (pixel >= mean) hash |= 1 else hash |= 0 + } + + if ((row +1) < dctDataWidth) { + val nextRow = row + 1 + //process each column + for (col <- (dctDataWidth - 1) to 0 by -1) { + hash <<= 1 + val pixel = imageDataFloat(nextRow)(col) + if (pixel >= mean) hash |= 1 else hash |= 0 + } + } + } + debug(s"Computed PHash: $hash from ${dctDataWidth * dctDataHeight} pixels") + hash } } diff --git a/src/test/resources/default.properties b/src/test/resources/default.properties index ffa4755..0752b0b 100644 --- a/src/test/resources/default.properties +++ b/src/test/resources/default.properties @@ -24,7 +24,7 @@ image.dhash.tolerence=8 #set to false if hashing images is taking too long image.phash.use=true image.phash.weight=1.0 -image.phash.precision=16 +image.phash.precision=32 image.phash.tolerence=8 #Default Thumbnail Settings diff --git a/src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala b/src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala index 5581ac5..efc6f8a 100644 --- a/src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala +++ b/src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala @@ -101,4 +101,158 @@ class HashServiceTest extends BaseTest { assert(HashService.areDhashSimilar(mediumHash,smallHash)) } + def ahashTestCase(filePath:String):Long = { + val sample = new File(filePath) + val image = ImageIO.read(sample) + HashService.getAhash(image) + } + + test("Benchmark AHash") { + info("Benchmarking AHash") + info("AHash Large Image 3684x2736") + val largeTime1 = getTime { ahashTestCase(TestParams.LargeSampleImage1) } + val largeTime2 = getTime { ahashTestCase(TestParams.LargeSampleImage1) } + val largeTime3 = getTime { ahashTestCase(TestParams.LargeSampleImage1) } + val largeTime4 = getTime { ahashTestCase(TestParams.LargeSampleImage1) } + val largeTime5 = getTime { ahashTestCase(TestParams.LargeSampleImage1) } + val largeMean = getMean(largeTime1, largeTime2, largeTime3, largeTime4, largeTime5) + info(s"The mean time of 5 tests for large was: $largeMean ms") + info("AHash Medium Image 1824x1368") + val mediumTime1 = getTime { ahashTestCase(TestParams.MediumSampleImage1) } + val mediumTime2 = getTime { ahashTestCase(TestParams.MediumSampleImage1) } + val mediumTime3 = getTime { ahashTestCase(TestParams.MediumSampleImage1) } + val mediumTime4 = getTime { ahashTestCase(TestParams.MediumSampleImage1) } + val mediumTime5 = getTime { ahashTestCase(TestParams.MediumSampleImage1) } + val mediumMean = getMean(mediumTime1, mediumTime2, mediumTime3, mediumTime4, mediumTime5) + info(s"The mean time of 5 tests for medium was: $mediumMean ms") + info("AHash Small Image 912x684") + val smallTime1 = getTime { ahashTestCase(TestParams.SmallSampleImage1) } + val smallTime2 = getTime { ahashTestCase(TestParams.SmallSampleImage1) } + val smallTime3 = getTime { ahashTestCase(TestParams.SmallSampleImage1) } + val smallTime4 = getTime { ahashTestCase(TestParams.SmallSampleImage1) } + val smallTime5 = getTime { ahashTestCase(TestParams.SmallSampleImage1) } + val smallMean = getMean(smallTime1, smallTime2, smallTime3, smallTime4, smallTime5) + info(s"The mean time of 5 tests for small was: $smallMean ms") + 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(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(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(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.areAhashSimilar(largeHash,mediumHash)) + assert(HashService.areAhashSimilar(largeHash,smallHash)) + assert(HashService.areAhashSimilar(mediumHash,smallHash)) + } + + def phashTestCase(filePath:String):Long = { + val sample = new File(filePath) + val image = ImageIO.read(sample) + HashService.getPhash(image) + } + + test("Benchmark PHash") { + info("Benchmarking PHash") + info("PHash Large Image 3684x2736") + val largeTime1 = getTime { phashTestCase(TestParams.LargeSampleImage1) } + val largeTime2 = getTime { phashTestCase(TestParams.LargeSampleImage1) } + val largeTime3 = getTime { phashTestCase(TestParams.LargeSampleImage1) } + val largeTime4 = getTime { phashTestCase(TestParams.LargeSampleImage1) } + val largeTime5 = getTime { phashTestCase(TestParams.LargeSampleImage1) } + val largeMean = getMean(largeTime1, largeTime2, largeTime3, largeTime4, largeTime5) + info(s"The mean time of 5 tests for large was: $largeMean ms") + info("PHash Medium Image 1824x1368") + val mediumTime1 = getTime { phashTestCase(TestParams.MediumSampleImage1) } + val mediumTime2 = getTime { phashTestCase(TestParams.MediumSampleImage1) } + val mediumTime3 = getTime { phashTestCase(TestParams.MediumSampleImage1) } + val mediumTime4 = getTime { phashTestCase(TestParams.MediumSampleImage1) } + val mediumTime5 = getTime { phashTestCase(TestParams.MediumSampleImage1) } + val mediumMean = getMean(mediumTime1, mediumTime2, mediumTime3, mediumTime4, mediumTime5) + info(s"The mean time of 5 tests for medium was: $mediumMean ms") + info("PHash Small Image 912x684") + val smallTime1 = getTime { phashTestCase(TestParams.SmallSampleImage1) } + val smallTime2 = getTime { phashTestCase(TestParams.SmallSampleImage1) } + val smallTime3 = getTime { phashTestCase(TestParams.SmallSampleImage1) } + val smallTime4 = getTime { phashTestCase(TestParams.SmallSampleImage1) } + val smallTime5 = getTime { phashTestCase(TestParams.SmallSampleImage1) } + val smallMean = getMean(smallTime1, smallTime2, smallTime3, smallTime4, smallTime5) + info(s"The mean time of 5 tests for small was: $smallMean ms") + 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(image) + debug(s"Testing that $hash = -9154589787976242949L") + assert(hash == -9154589787976242949L) + } + + 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(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(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.arePhashSimilar(largeHash,mediumHash)) + assert(HashService.arePhashSimilar(largeHash,smallHash)) + assert(HashService.arePhashSimilar(mediumHash,smallHash)) + } + }